Skip to content

Commit

Permalink
Add description to typeahead suggestions
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathonherbert committed Jul 10, 2024
1 parent e6303b1 commit 6f90c82
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 97 deletions.
2 changes: 1 addition & 1 deletion src/main/scala/lang/Cql.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class Cql:
implicit val ec: scala.concurrent.ExecutionContext =
scala.concurrent.ExecutionContext.global
val guardianContentClient = new GuardianContentClient("test")
val typeaheadClient = new TypeaheadQueryCapiClient(guardianContentClient)
val typeaheadClient = new TypeaheadQueryClientTest()
val typeahead = new Typeahead(typeaheadClient)

def run(program: String): Future[CqlResult] =
Expand Down
264 changes: 172 additions & 92 deletions src/main/scala/lang/Typeahead.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ package cql.lang

import scala.concurrent.Future
import concurrent.ExecutionContext.Implicits.global
import cql.lang.QueryField
import cql.lang.QueryList
import cql.lang.QueryOutputModifier

case class TypeaheadSuggestion(
from: Int,
Expand All @@ -18,10 +15,22 @@ case class TypeaheadSuggestion(

sealed trait Suggestions

case class TextSuggestion(suggestions: List[TextSuggestionOption]) extends Suggestions
case class TextSuggestionOption(label: String, value: String)
case class TextSuggestion(suggestions: List[TextSuggestionOption])
extends Suggestions

case class DateSuggestion(validFrom: Option[String], validTo: Option[String]) extends Suggestions
case class TextSuggestionOption(
label: String,
value: String,
description: String
)

case class DateSuggestion(validFrom: Option[String], validTo: Option[String])
extends Suggestions

type TypeaheadResolver = (String => Future[List[TextSuggestionOption]]) |
List[TextSuggestionOption]

type TypeaheadType = "TEXT" | "DATE"

class Typeahead(client: TypeaheadQueryClient) {
private val typeaheadTokenResolverMap = Map(
Expand All @@ -30,70 +39,125 @@ class Typeahead(client: TypeaheadQueryClient) {
TokenType.QUERY_VALUE -> suggestFieldValue
)

case class TypeaheadField(
name: String,
description: String,
resolver: TypeaheadResolver,
suggestionType: "TEXT" | "DATE" = "TEXT"
):
def resolveSuggestions(str: String): Future[List[TextSuggestionOption]] =
resolver match {
case list: List[TextSuggestionOption] =>
Future.successful(list.filter { _.label.contains(str) })
case resolverFn: (String => Future[List[TextSuggestionOption]]) =>
resolverFn(str)
}

def toSuggestionOption: TextSuggestionOption =
TextSuggestionOption(name, name, description)

private val typeaheadFieldResolverMap = Map(
"tag" -> ("Tag", suggestTags),
"section" -> ("Section", suggestSections)
"tag" -> TypeaheadField(
"Tag",
"Search by content tags, e.g. sport/football",
suggestTags
),
"section" -> TypeaheadField(
"Section",
"Search by content sections, e.g. section/news",
suggestSections
)
)

private val typeaheadFieldResolverEntries = TextSuggestion(
typeaheadFieldResolverMap.map { case (value, (label, _)) =>
TextSuggestionOption(label, value)
typeaheadFieldResolverMap.map {
case (value, TypeaheadField(label, description, _, _)) =>
TextSuggestionOption(label, value, description)
}.toList
)

private val typeaheadOutputModifierResolverMap
: Map[String, ("TEXT" | "DATE", List[String])] = Map(
"show-fields" -> ("TEXT", List(
"all",
"trailText",
"headline",
"showInRelatedContent",
"body",
"lastModified",
"hasStoryPackage",
"score",
"standfirst",
"shortUrl",
"thumbnail",
"wordcount",
"commentable",
"isPremoderated",
"allowUgc",
"byline",
"publication",
"internalPageCode",
"productionOffice",
"shouldHideAdverts",
"liveBloggingNow",
"commentCloseDate",
"starRating"
)),
"from-date" -> ("DATE", List.empty),
"to-date" -> ("DATE", List.empty)
private val typeaheadOutputModifierResolvers = List(
TypeaheadField(
"show-fields",
"Determine the list of fields to return",
List(
TextSuggestionOption("all", "all", "Description"),
TextSuggestionOption("trailText", "trailText", "Description"),
TextSuggestionOption("headline", "headline", "Description"),
TextSuggestionOption(
"showInRelatedContent",
"showInRelatedContent",
"Description"
),
TextSuggestionOption("body", "body", "Description"),
TextSuggestionOption("lastModified", "lastModified", "Description"),
TextSuggestionOption(
"hasStoryPackage",
"hasStoryPackage",
"Description"
),
TextSuggestionOption("score", "score", "Description"),
TextSuggestionOption("standfirst", "standfirst", "Description"),
TextSuggestionOption("shortUrl", "shortUrl", "Description"),
TextSuggestionOption("thumbnail", "thumbnail", "Description"),
TextSuggestionOption("wordcount", "wordcount", "Description"),
TextSuggestionOption("commentable", "commentable", "Description"),
TextSuggestionOption("isPremoderated", "isPremoderated", "Description"),
TextSuggestionOption("allowUgc", "allowUgc", "Description"),
TextSuggestionOption("byline", "byline", "Description"),
TextSuggestionOption("publication", "publication", "Description"),
TextSuggestionOption(
"internalPageCode",
"internalPageCode",
"Description"
),
TextSuggestionOption(
"productionOffice",
"productionOffice",
"Description"
),
TextSuggestionOption(
"shouldHideAdverts",
"shouldHideAdverts",
"Description"
),
TextSuggestionOption(
"liveBloggingNow",
"liveBloggingNow",
"Description"
),
TextSuggestionOption(
"commentCloseDate",
"commentCloseDate",
"Description"
),
TextSuggestionOption("starRating", "starRating", "Description")
)
),
TypeaheadField("from-date", "The date to search from", List.empty),
TypeaheadField("to-date", "The date to search to", List.empty)
)

private val typeaheadOutputModifierResolverEntries =
typeaheadOutputModifierResolverMap.keys.map { case key =>
TextSuggestionOption(key, key)
}.toList

def getSuggestions(
program: QueryList
): Future[List[TypeaheadSuggestion]] =
val eventuallySuggestions = program.exprs.collect {
case q: QueryField =>
suggestQueryField(q)
case q: QueryOutputModifier =>
suggestQueryOutputModifier(q).map(Future.successful)
}.flatten

Future.sequence(eventuallySuggestions)
Future
.traverse(program.exprs) {
case q: QueryField =>
suggestQueryField(q)
case q: QueryOutputModifier =>
suggestQueryOutputModifier(q)
case _ => Future.successful(List.empty)
}
.map(_.flatten)

private def suggestQueryField(q: QueryField) =
private def suggestQueryField(
q: QueryField
): Future[List[TypeaheadSuggestion]] =
q match {
case QueryField(keyToken, None) =>
List(
Future.successful(
Future.successful(
List(
TypeaheadSuggestion(
keyToken.start,
keyToken.end,
Expand Down Expand Up @@ -124,39 +188,49 @@ class Typeahead(client: TypeaheadQueryClient) {
TextSuggestion(suggestions)
)
}
List(keySuggestions, valueSuggestions)
Future.sequence(List(keySuggestions, valueSuggestions))
}

private def suggestQueryOutputModifier(q: QueryOutputModifier) =
private def suggestQueryOutputModifier(
q: QueryOutputModifier
): Future[List[TypeaheadSuggestion]] =
q match {
case QueryOutputModifier(keyToken, None) =>
List(
suggestOutputModifierKey(keyToken.literal.getOrElse("")).map {
suggestions =>
List(
TypeaheadSuggestion(
keyToken.start,
keyToken.end,
":",
suggestions
)
)
}
case QueryOutputModifier(keyToken, Some(valueToken)) =>
val keySuggestion = suggestOutputModifierKey(
keyToken.literal.getOrElse("")
).map { suggestion =>
TypeaheadSuggestion(
keyToken.start,
keyToken.end,
":",
suggestOutputModifierKey(keyToken.literal.getOrElse(""))
suggestion
)
)
case QueryOutputModifier(keyToken, Some(valueToken)) =>
val keySuggestions =
}
val valueSuggestion = suggestOutputModifierValue(
keyToken.literal.getOrElse(""),
valueToken.literal.getOrElse("")
).map { suggestions =>
TypeaheadSuggestion(
keyToken.start,
keyToken.end,
":",
suggestOutputModifierKey(keyToken.literal.getOrElse(""))
)
val valueSuggestions =
TypeaheadSuggestion(
valueToken.start,
valueToken.end,
" ",
suggestOutputModifierValue(
keyToken.literal.getOrElse(""),
valueToken.literal.getOrElse("")
)
suggestions
)
List(keySuggestions, valueSuggestions)
}

Future.sequence(List(keySuggestion, valueSuggestion))
}

private def suggestFieldKey(str: String): TextSuggestion =
Expand All @@ -174,34 +248,40 @@ class Typeahead(client: TypeaheadQueryClient) {
): Future[List[TextSuggestionOption]] =
typeaheadFieldResolverMap
.get(key)
.map(_._2(str))
.map(_.resolveSuggestions(str))
.getOrElse(Future.successful(List.empty))

private def suggestOutputModifierKey(str: String): TextSuggestion =
TextSuggestion(
typeaheadOutputModifierResolverEntries.filter(
_.value.contains(str.toLowerCase())
private def suggestOutputModifierKey(str: String): Future[TextSuggestion] =
Future.successful(
TextSuggestion(
typeaheadOutputModifierResolvers
.filter(
_.name.contains(str.toLowerCase())
)
.map(_.toSuggestionOption)
)
)

private def suggestOutputModifierValue(
key: String,
str: String
): TextSuggestion | DateSuggestion =
typeaheadOutputModifierResolverMap.get(key) match
case Some("TEXT", suggestions) =>
TextSuggestion(
suggestions
.filter(
_.contains(
str.toLowerCase()
): Future[TextSuggestion | DateSuggestion] =
typeaheadOutputModifierResolvers.find(_.name == key) match
case Some(typeaheadField) if typeaheadField.suggestionType == "TEXT" =>
typeaheadField.resolveSuggestions(str).map { suggestions =>
TextSuggestion(
suggestions
.filter(
_.label.contains(
str.toLowerCase()
)
)
)
.map { str => TextSuggestionOption(str, str) }
)
)
}
// Todo: date validation based on rest of AST
case Some("DATE", _) => DateSuggestion(None, None)
case None => TextSuggestion(List.empty)
case Some(typeaheadField) if typeaheadField.suggestionType == "DATE" =>
Future.successful(DateSuggestion(None, None))
case _ => Future.successful(TextSuggestion(List.empty))

private def suggestTags(str: String) =
client.getTags(str)
Expand Down
8 changes: 4 additions & 4 deletions src/main/scala/lang/TypeaheadQueryClientCapi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class TypeaheadQueryCapiClient(client: GuardianContentClient)
case str => ContentApiClient.tags.q(str)
client.getResponse(query).map { response =>
response.results.map { tag =>
TextSuggestionOption(tag.webTitle, tag.id)
TextSuggestionOption(tag.webTitle, tag.id, tag.description.getOrElse(""))
}.toList
}

Expand All @@ -22,19 +22,19 @@ class TypeaheadQueryCapiClient(client: GuardianContentClient)
case str => ContentApiClient.sections.q(str)
client.getResponse(query).map { response =>
response.results.map { section =>
TextSuggestionOption(section.webTitle, section.id)
TextSuggestionOption(section.webTitle, section.id, section.webTitle)
}.toList
}
}

class TypeaheadQueryClientTest extends TypeaheadQueryClient {
def getTags(str: String): Future[List[TextSuggestionOption]] =
Future.successful(
List(TextSuggestionOption("Tags are magic", "tags-are-magic"))
List(TextSuggestionOption("Tags are magic", "tags-are-magic", "A magic tag"))
)

def getSections(str: String): Future[List[TextSuggestionOption]] =
Future.successful(
List(TextSuggestionOption("Also sections", "sections-are-magic"))
List(TextSuggestionOption("Also sections", "sections-are-magic", "Sections are less magic"))
)
}

0 comments on commit 6f90c82

Please sign in to comment.