diff --git a/src/main/scala/Handler.scala b/src/main/scala/Handler.scala index c77442f..ba04621 100644 --- a/src/main/scala/Handler.scala +++ b/src/main/scala/Handler.scala @@ -5,7 +5,6 @@ import com.amazonaws.services.lambda.runtime.events.{ APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent } -import io.circe.generic.auto._ import io.circe.syntax._ import util.Logging @@ -21,8 +20,7 @@ class Handler APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent ] - with Logging - with QueryJson { + with Logging { private implicit val ec: scala.concurrent.ExecutionContext = scala.concurrent.ExecutionContext.global diff --git a/src/main/scala/HttpServer.scala b/src/main/scala/HttpServer.scala index efda4b8..79bd91a 100644 --- a/src/main/scala/HttpServer.scala +++ b/src/main/scala/HttpServer.scala @@ -12,7 +12,7 @@ import scala.util.Try import cql.lang.{Cql, Typeahead, TypeaheadHelpersCapi} import com.gu.contentapi.client.GuardianContentClient -object HttpServer extends QueryJson { +object HttpServer { val guardianContentClient = new GuardianContentClient("test") val typeaheadHelpers = new TypeaheadHelpersCapi(guardianContentClient) val typeahead = new Typeahead( diff --git a/src/main/scala/QueryJson.scala b/src/main/scala/QueryJson.scala deleted file mode 100644 index 194818d..0000000 --- a/src/main/scala/QueryJson.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cql - -import io.circe._ -import io.circe.generic.semiauto.deriveEncoder -import io.circe.Encoder -import io.circe.syntax._ - -import cql.lang.{ - Token, - TokenType, - TypeaheadSuggestion, - TextSuggestion, - TextSuggestionOption, - DateSuggestion, - QueryList, - QueryBinary, - QueryField, - QueryOutputModifier, - QueryGroup, - QueryStr, - QueryContent, - CqlResult -} - -trait QueryJson { - implicit val typeaheadSuggestions: Encoder[TypeaheadSuggestion] = - deriveEncoder[TypeaheadSuggestion] - implicit val typeaheadTextSuggestion: Encoder[TextSuggestion] = - deriveEncoder[TextSuggestion] - implicit val typeaheadDateSuggestion: Encoder[DateSuggestion] = - deriveEncoder[DateSuggestion] - implicit val textSuggestionOption: Encoder[TextSuggestionOption] = - deriveEncoder[TextSuggestionOption] - - implicit val cqlResultEncoder: Encoder[CqlResult] = deriveEncoder[CqlResult] - implicit val queryListEncoder: Encoder[QueryList] = Encoder.instance { list => - val arr = list.exprs.map { - case q: QueryBinary => q.asJson - case q: QueryField => q.asJson - case q: QueryOutputModifier => q.asJson - } - Json.obj("type" -> "QueryList".asJson, "content" -> Json.arr(arr*)) - } - implicit val queryGroupEncoder: Encoder[QueryGroup] = Encoder.instance { - group => - group.content.asJson.deepMerge(Json.obj("type" -> "QueryGroup".asJson)) - } - implicit val queryStrEncoder: Encoder[QueryStr] = Encoder.instance { - queryStr => - Json.obj( - "type" -> "QueryStr".asJson, - "searchExpr" -> queryStr.searchExpr.asJson - ) - } - implicit val queryFieldEncoder: Encoder[QueryField] = Encoder.instance { - queryField => - Json.obj( - "type" -> "QueryField".asJson, - "key" -> queryField.key.asJson, - "value" -> queryField.value.asJson - ) - } - implicit val queryOutputModifierEncoder: Encoder[QueryOutputModifier] = - Encoder.instance { queryField => - Json.obj( - "type" -> "QueryOutputModifier".asJson, - "key" -> queryField.key.asJson, - "value" -> queryField.value.asJson - ) - } - implicit val tokenEncoder: Encoder[Token] = Encoder.instance { token => - Json.obj( - "type" -> "Token".asJson, - "tokenType" -> token.tokenType.toString.asJson, - "lexeme" -> token.lexeme.asJson, - "start" -> token.start.asJson, - "end" -> token.end.asJson, - "literal" -> token.literal.asJson - ) - } - implicit val queryContentEncoder: Encoder[QueryContent] = Encoder.instance { - queryContent => - val content = queryContent.content match { - case q: QueryStr => q.asJson - case q: QueryBinary => q.asJson - case q: QueryGroup => q.asJson - } - - content.deepMerge(Json.obj("type" -> "QueryContent".asJson)) - } - implicit val queryBinaryEncoder: Encoder[QueryBinary] = Encoder.instance { - queryBinary => - Json.obj( - "type" -> "QueryBinary".asJson, - ("left", queryBinary.left.asJson), - ("right", queryBinary.right.asJson) - ) - } -} diff --git a/src/main/scala/lang/Ast.scala b/src/main/scala/lang/Ast.scala index d360fcc..8e0fc19 100644 --- a/src/main/scala/lang/Ast.scala +++ b/src/main/scala/lang/Ast.scala @@ -1,16 +1,93 @@ package cql.lang +import io.circe.Encoder +import io.circe.syntax._ +import io.circe.Json + trait Query +object QueryList { + implicit val encoder: Encoder[QueryList] = Encoder.instance { list => + val arr = list.exprs.map { + case q: QueryBinary => q.asJson + case q: QueryField => q.asJson + case q: QueryOutputModifier => q.asJson + } + Json.obj("type" -> "QueryList".asJson, "content" -> Json.arr(arr*)) + } +} case class QueryList( exprs: List[QueryBinary | QueryField | QueryOutputModifier] ) extends Query + +object QueryBinary { + implicit val encoder: Encoder[QueryBinary] = Encoder.instance { queryBinary => + Json.obj( + "type" -> "QueryBinary".asJson, + ("left", queryBinary.left.asJson), + ("right", queryBinary.right.asJson) + ) + } +} case class QueryBinary( left: QueryContent, right: Option[(Token, QueryContent)] = None ) + +object QueryContent { + implicit val encoder: Encoder[QueryContent] = Encoder.instance { + queryContent => + val content = queryContent.content match { + case q: QueryStr => q.asJson + case q: QueryBinary => q.asJson + case q: QueryGroup => q.asJson + } + + content.deepMerge(Json.obj("type" -> "QueryContent".asJson)) + } +} + case class QueryContent(content: QueryStr | QueryBinary | QueryGroup) + +object QueryGroup { + implicit val encoder: Encoder[QueryGroup] = Encoder.instance { group => + group.content.asJson.deepMerge(Json.obj("type" -> "QueryGroup".asJson)) + } +} + case class QueryGroup(content: QueryBinary) + +object QueryStr { + implicit val encoder: Encoder[QueryStr] = Encoder.instance { queryStr => + Json.obj( + "type" -> "QueryStr".asJson, + "searchExpr" -> queryStr.searchExpr.asJson + ) + } +} + case class QueryStr(searchExpr: String) extends Query +object QueryField { + implicit val encoder: Encoder[QueryField] = Encoder.instance { queryField => + Json.obj( + "type" -> "QueryField".asJson, + "key" -> queryField.key.asJson, + "value" -> queryField.value.asJson + ) + } +} + case class QueryField(key: Token, value: Option[Token]) extends Query + +object QueryOutputModifier { + implicit val encoder: Encoder[QueryOutputModifier] = + Encoder.instance { queryField => + Json.obj( + "type" -> "QueryOutputModifier".asJson, + "key" -> queryField.key.asJson, + "value" -> queryField.value.asJson + ) + } +} + case class QueryOutputModifier(key: Token, value: Option[Token]) extends Query diff --git a/src/main/scala/lang/Cql.scala b/src/main/scala/lang/Cql.scala index a1db7f2..0ab05c2 100644 --- a/src/main/scala/lang/Cql.scala +++ b/src/main/scala/lang/Cql.scala @@ -1,12 +1,17 @@ package cql.lang import scala.util.{Failure, Success, Try} -import io.circe.generic.semiauto.* - import scala.concurrent.Future +import io.circe.generic.semiauto.deriveEncoder +import io.circe.Encoder case class CqlError(message: String, position: Option[Int] = None) +object CqlError { + implicit val typeaheadSuggestions: Encoder[CqlError] = + deriveEncoder[CqlError] +} + case class CqlResult( tokens: List[Token], ast: Option[QueryList], @@ -14,9 +19,13 @@ case class CqlResult( // Map from tokenType to a map of literals and their suggestions. // Avoiding TokenType as type to avoid serialisation shenanigans in prototype. suggestions: List[TypeaheadSuggestion], - error: Option[CqlError] = None + error: Option[CqlError] ) +object CqlResult { + implicit val cqlResultEncoder: Encoder[CqlResult] = deriveEncoder[CqlResult] +} + class Cql(typeahead: Typeahead): implicit val ec: scala.concurrent.ExecutionContext = scala.concurrent.ExecutionContext.global @@ -31,7 +40,13 @@ class Cql(typeahead: Typeahead): typeahead.getSuggestions(expr).map { suggestions => Try { CapiQueryString.build(expr) } match case Success(capiQueryStr) => - CqlResult(tokens, Some(expr), Some(capiQueryStr), suggestions) + CqlResult( + tokens, + Some(expr), + Some(capiQueryStr), + suggestions, + None + ) case Failure(e: Throwable) => CqlResult( tokens, @@ -43,7 +58,8 @@ class Cql(typeahead: Typeahead): } case Failure(e) => val error = e match { - case ParseError(position, message) => CqlError(message, Some(position)) + case ParseError(position, message) => + CqlError(message, Some(position)) case e: Throwable => CqlError(e.getMessage) } diff --git a/src/main/scala/lang/Token.scala b/src/main/scala/lang/Token.scala index 5394bce..5035064 100644 --- a/src/main/scala/lang/Token.scala +++ b/src/main/scala/lang/Token.scala @@ -1,5 +1,9 @@ package cql.lang +import io.circe.Encoder +import io.circe.Json +import io.circe.syntax._ + enum TokenType: // Single-character tokens. case PLUS, COLON, AT, LEFT_BRACKET, RIGHT_BRACKET, @@ -24,3 +28,14 @@ object Token: "AND" -> TokenType.AND, "OR" -> TokenType.OR ) + + implicit val tokenEncoder: Encoder[Token] = Encoder.instance { token => + Json.obj( + "type" -> "Token".asJson, + "tokenType" -> token.tokenType.toString.asJson, + "lexeme" -> token.lexeme.asJson, + "start" -> token.start.asJson, + "end" -> token.end.asJson, + "literal" -> token.literal.asJson + ) + } diff --git a/src/main/scala/lang/Typeahead.scala b/src/main/scala/lang/Typeahead.scala index 978bc8f..079113e 100644 --- a/src/main/scala/lang/Typeahead.scala +++ b/src/main/scala/lang/Typeahead.scala @@ -2,6 +2,8 @@ package cql.lang import scala.concurrent.Future import concurrent.ExecutionContext.Implicits.global +import io.circe.Encoder +import io.circe.generic.semiauto.deriveEncoder case class TypeaheadSuggestion( from: Int, @@ -13,20 +15,42 @@ case class TypeaheadSuggestion( suggestions: Suggestions ) +object TypeaheadSuggestion { + implicit val encoder: Encoder[TypeaheadSuggestion] = + deriveEncoder[TypeaheadSuggestion] +} + sealed trait Suggestions case class TextSuggestion(suggestions: List[TextSuggestionOption]) extends Suggestions +object TextSuggestion { + implicit val encoder: Encoder[TextSuggestion] = + deriveEncoder[TextSuggestion] + +} + case class TextSuggestionOption( label: String, value: String, description: String ) +object TextSuggestionOption { + + implicit val encoder: Encoder[TextSuggestionOption] = + deriveEncoder[TextSuggestionOption] +} + case class DateSuggestion(validFrom: Option[String], validTo: Option[String]) extends Suggestions +object DateSuggestion { + implicit val encoder: Encoder[DateSuggestion] = + deriveEncoder[DateSuggestion] +} + type TypeaheadResolver = (String => Future[List[TextSuggestionOption]]) | List[TextSuggestionOption] diff --git a/src/test/scala/lang/ScannerTest.scala b/src/test/scala/lang/ScannerTest.scala index 6fbea3d..93831de 100644 --- a/src/test/scala/lang/ScannerTest.scala +++ b/src/test/scala/lang/ScannerTest.scala @@ -91,7 +91,9 @@ class ScannerTest extends BaseTest { assert(tokens === expectedTokens) } - it("should yield a query field value token when a query meta value is incomplete") { + it( + "should yield a query field value token when a query meta value is incomplete" + ) { val scanner = new Scanner("""example +tag:""") val tokens = scanner.scanTokens val expectedTokens = List( @@ -107,7 +109,13 @@ class ScannerTest extends BaseTest { val scanner = new Scanner("""@show-fields:all""") val tokens = scanner.scanTokens val expectedTokens = List( - Token(TokenType.QUERY_OUTPUT_MODIFIER_KEY, "@show-fields", Some("show-fields"), 0, 11), + Token( + TokenType.QUERY_OUTPUT_MODIFIER_KEY, + "@show-fields", + Some("show-fields"), + 0, + 11 + ), Token( TokenType.QUERY_VALUE, ":all", diff --git a/src/test/scala/lang/TestTypeaheadHelpers.scala b/src/test/scala/lang/TestTypeaheadHelpers.scala index 06f55c7..4c3c035 100644 --- a/src/test/scala/lang/TestTypeaheadHelpers.scala +++ b/src/test/scala/lang/TestTypeaheadHelpers.scala @@ -3,7 +3,7 @@ package cql.lang import scala.concurrent.Future class TestTypeaheadHelpers { - val fieldResolvers = List( + val fieldResolvers = List( TypeaheadField( "tag", "Tag", @@ -19,17 +19,30 @@ class TestTypeaheadHelpers { ) val outputModifierResolvers = List( - TypeaheadField("from-date", "From date", "The date to search from", List.empty), - TypeaheadField("to-date","To date", "The date to search to", List.empty) + TypeaheadField( + "from-date", + "From date", + "The date to search from", + List.empty + ), + TypeaheadField("to-date", "To date", "The date to search to", List.empty) ) private def getTags(str: String): Future[List[TextSuggestionOption]] = Future.successful( - List(TextSuggestionOption("Tags are magic", "tags-are-magic", "A magic tag")) + List( + TextSuggestionOption("Tags are magic", "tags-are-magic", "A magic tag") + ) ) private def getSections(str: String): Future[List[TextSuggestionOption]] = Future.successful( - List(TextSuggestionOption("Also sections", "sections-are-magic", "Sections are less magic")) + List( + TextSuggestionOption( + "Also sections", + "sections-are-magic", + "Sections are less magic" + ) + ) ) } diff --git a/src/test/scala/lang/TypeaheadTest.scala b/src/test/scala/lang/TypeaheadTest.scala index 6c57ebd..8986fdd 100644 --- a/src/test/scala/lang/TypeaheadTest.scala +++ b/src/test/scala/lang/TypeaheadTest.scala @@ -7,7 +7,10 @@ class TypeaheadTest extends BaseTest { describe("typeahead") { val typeaheadQueryClient = new TestTypeaheadHelpers() val typeahead = - new Typeahead(typeaheadQueryClient.fieldResolvers, typeaheadQueryClient.outputModifierResolvers) + new Typeahead( + typeaheadQueryClient.fieldResolvers, + typeaheadQueryClient.outputModifierResolvers + ) it("should give no typeahead where none is warranted") { typeahead.getSuggestions(QueryList(List.empty)).map { result =>