Skip to content

Commit

Permalink
feat: Fill out more of the API.
Browse files Browse the repository at this point in the history
  • Loading branch information
vxern committed Jun 6, 2024
1 parent 6cda031 commit d0d00ac
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 13 deletions.
177 changes: 168 additions & 9 deletions src/tatoeba/search.gleam
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import gleam/dynamic
import gleam/dynamic.{type Dynamic, bool, field, int, list, optional, string}
import gleam/http
import gleam/http/request
import gleam/httpc
Expand All @@ -11,7 +11,7 @@ import tatoeba/api
import tatoeba/search/sentence.{type SentenceOptions} as sentence_options
import tatoeba/search/translation.{type TranslationOptions} as translation_options
import tatoeba/search/utils
import tatoeba/sentence.{type Sentence}
import tatoeba/sentence.{type Sentence, sentence}

/// The sort strategy used to arrange sentences from the result of the search query.
///
Expand All @@ -34,6 +34,31 @@ pub type SortStrategy {
Random
}

/// Checks to see whether a `Dynamic` value is a sort strategy, and returns the sort strategy
/// if it is.
///
fn sort_strategy(
from data: Dynamic,
) -> Result(SortStrategy, List(dynamic.DecodeError)) {
use string <- result.try(data |> string())

case string {
"relevance" -> Ok(Relevance)
"words" -> Ok(FewestWordsFirst)
"created" -> Ok(LastCreatedFirst)
"modified" -> Ok(LastModifiedFirst)
"random" -> Ok(Random)
string ->
Error([
dynamic.DecodeError(
expected: "One of: \"relevance\", \"words\", \"created\", \"modified\", \"random\"",
found: string,
path: [],
),
])
}
}

/// Converts the sort strategy to its string representation ready to encode
/// in the query.
///
Expand Down Expand Up @@ -127,20 +152,154 @@ pub fn to_query_parameters(options: SearchOptions) -> List(#(String, String)) {
])
}

// TODO(vxern): Document.
pub type Finder {
// TODO(vxern): Document.
All
}

/// Checks to see whether a `Dynamic` value is a finder, and returns the finder if it is.
///
fn finder(
from data: dynamic.Dynamic,
) -> Result(Finder, List(dynamic.DecodeError)) {
use string <- result.try(data |> string())

case string {
"all" -> Ok(All)
string ->
Error([dynamic.DecodeError(expected: "\"all\"", found: string, path: [])])
}
}

/// Represents data describing how the search results are paged.
///
pub type Paging {
Paging(
// TODO(vxern): Document.
finder: Finder,
// TODO(vxern): Document.
page: Int,
// TODO(vxern): Document.
current_page: Int,
// TODO(vxern): Document.
page_count: Int,
// TODO(vxern): Document.
per_page: Int,
// TODO(vxern): Document.
start: Int,
// TODO(vxern): Document.
end: Int,
// TODO(vxern): Document.
previous_page: Bool,
// TODO(vxern): Document.
next_page: Bool,
/// The sort strategy used in the search query.
sort_strategy: Option(SortStrategy),
// TODO(vxern): Document.
direction: Option(Bool),
// TODO(vxern): Document.
limit: Option(sentence.Unknown),
// TODO(vxern): Document.
sort_default: Bool,
// TODO(vxern): Document.
direction_default: Bool,
// TODO(vxern): Document.
scope: Option(sentence.Unknown),
// TODO(vxern): Document.
complete_sort: List(sentence.Unknown),
)
}

/// Checks to see whether a `Dynamic` value contains paging data, and returns the
/// data if it does.
///
fn paging(from data: Dynamic) -> Result(Paging, List(dynamic.DecodeError)) {
use finder <- result.try(data |> field("finder", finder))
use page <- result.try(data |> field("page", int))
use current_page <- result.try(data |> field("current", int))
use page_count <- result.try(data |> field("count", int))
use per_page <- result.try(data |> field("per_page", int))
use start <- result.try(data |> field("start", int))
use end <- result.try(data |> field("end", int))
use previous_page <- result.try(data |> field("previous_page", bool))
use next_page <- result.try(data |> field("next_page", bool))
use sort_strategy <- result.try(
data |> field("sort", optional(sort_strategy)),
)
use direction <- result.try(
data |> field("direction", optional(dynamic.dynamic)),
)
use limit <- result.try(data |> field("limit", optional(dynamic.dynamic)))
use sort_default <- result.try(data |> field("sort_default", bool))
use direction_default <- result.try(data |> field("direction_default", bool))
use scope <- result.try(data |> field("scope", optional(dynamic.dynamic)))
use complete_sort <- result.try(
data |> field("complete_sort", list(dynamic.dynamic)),
)

Paging(
finder: finder,
page: page,
current_page: current_page,
page_count: page_count,
per_page: per_page,
start: start,
end: end,
previous_page: previous_page,
next_page: next_page,
sort_strategy: sort_strategy,
direction: direction,
limit: limit,
sort_default: sort_default,
direction_default: direction_default,
scope: scope,
complete_sort: complete_sort,
)
}

/// Represents the results of a search query run over the Tatoeba corpus.
///
pub type SearchResults {
SearchResults(
/// The state of paging of the results.
paging: Paging,
/// The sentences found as a result of the query.
results: List(Sentence),
)
}

/// Checks to see whether a `Dynamic` value contains search results, and returns the
/// results value if it does.
///
fn results(
from data: Dynamic,
) -> Result(SearchResults, List(dynamic.DecodeError)) {
use paging <- result.try(data |> field("paging", paging))
use results <- result.try(data |> field("results", list(sentence)))

Ok(SearchResults(paging: paging, results: results))
}

/// Runs a search query using the passed `options` to filter the results.
///
pub fn run(options: SearchOptions) -> Result(List(Sentence), Nil) {
pub fn run(options: SearchOptions) -> Result(SearchResults, String) {
let request =
api.new_request_to("/search")
|> request.set_method(http.Get)
|> request.set_query(to_query_parameters(options))

use response <- result.try(httpc.send(request) |> result.nil_error)
use payload <- result.try(
json.decode(response.body, dynamic.dynamic) |> result.nil_error,
use response <- result.try(
httpc.send(request)
|> result.map_error(fn(_) { "Failed to send request to Tatoeba." }),
)
use results <- result.try(
json.decode(response.body, results)
|> result.map_error(fn(error) {
"Failed to decode sentence data: "
<> dynamic.classify(dynamic.from(error))
}),
)

io.debug(payload)

Ok([])
Ok(results)
}
6 changes: 4 additions & 2 deletions src/tatoeba/sentence.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ pub type TranscriptionType {
AlternativeScript
}

// TODO(vxern): Document.
pub fn transcription_type(
from data: Dynamic,
) -> Result(TranscriptionType, List(dynamic.DecodeError)) {
Expand Down Expand Up @@ -465,7 +466,9 @@ fn list_id(from data: Dynamic) -> Result(Int, List(dynamic.DecodeError)) {
/// Checks to see whether a `Dynamic` value is a sentence, and returns the sentence
/// if it is.
///
fn sentence(from data: Dynamic) -> Result(Sentence, List(dynamic.DecodeError)) {
pub fn sentence(
from data: Dynamic,
) -> Result(Sentence, List(dynamic.DecodeError)) {
use id <- result.try(data |> field("id", int))
use text <- result.try(data |> field("text", string))
use language <- result.try(data |> field("lang", optional(string)))
Expand Down Expand Up @@ -547,7 +550,6 @@ pub fn get(id id: Int) -> Result(Option(Sentence), String) {
use sentence <- result.try(
json.decode(response.body, sentence)
|> result.map_error(fn(error) {
io.debug(error)
"Failed to decode sentence data: "
<> dynamic.classify(dynamic.from(error))
}),
Expand Down
4 changes: 2 additions & 2 deletions src/tatoeba/utils.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import gleam/result
/// Converts a stringified `Int` representation of a boolean value to a `Bool`.
///
pub fn stringified_int_bool(
dynamic: Dynamic,
from data: Dynamic,
) -> Result(Bool, List(dynamic.DecodeError)) {
use string <- result.try(dynamic |> string())
use string <- result.try(data |> string())

case string {
"1" -> Ok(True)
Expand Down

0 comments on commit d0d00ac

Please sign in to comment.