diff --git a/src/tatoeba/search.gleam b/src/tatoeba/search.gleam index ec3272f..90ffc6f 100644 --- a/src/tatoeba/search.gleam +++ b/src/tatoeba/search.gleam @@ -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 @@ -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. /// @@ -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. /// @@ -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) } diff --git a/src/tatoeba/sentence.gleam b/src/tatoeba/sentence.gleam index c7489d1..dc34e8f 100644 --- a/src/tatoeba/sentence.gleam +++ b/src/tatoeba/sentence.gleam @@ -61,6 +61,7 @@ pub type TranscriptionType { AlternativeScript } +// TODO(vxern): Document. pub fn transcription_type( from data: Dynamic, ) -> Result(TranscriptionType, List(dynamic.DecodeError)) { @@ -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))) @@ -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)) }), diff --git a/src/tatoeba/utils.gleam b/src/tatoeba/utils.gleam index 36f96ea..d322c6c 100644 --- a/src/tatoeba/utils.gleam +++ b/src/tatoeba/utils.gleam @@ -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)