diff --git a/src/at/lexicon.gleam b/src/at/lexicon.gleam new file mode 100644 index 0000000..73672b1 --- /dev/null +++ b/src/at/lexicon.gleam @@ -0,0 +1,814 @@ +// IMPORTS --------------------------------------------------------------------- + +import at/at_uri +import at/cid +import at/did +import at/handle +import at/internal/decoding +import at/lexicon/schema.{type Schema} +import at/nsid.{type Nsid} +import at/rkey +import at/tid +import gleam/bit_array +import gleam/bool +import gleam/dict.{type Dict} +import gleam/dynamic.{type Dynamic} +import gleam/dynamic/decode.{type Decoder} +import gleam/int +import gleam/list +import gleam/option.{type Option, None, Some} +import gleam/set.{type Set} +import gleam/string +import gleam/time/timestamp +import gleam/uri + +// TYPES ----------------------------------------------------------------------- + +/// +/// +pub type Lexicon { + Lexicon( + version: Int, + id: Nsid, + description: Option(String), + main: Option(MainDefinition), + defs: Dict(String, Schema), + ) +} + +/// +/// +pub type MainDefinition { + /// + /// + Record( + description: Option(String), + key: RecordKey, + properties: Dict(String, Schema), + required: Set(String), + nullable: Set(String), + ) + + /// + /// + Query( + description: Option(String), + parameters: Option(XrpcParams), + output: Option(XrpcBody), + errors: List(XrpcError), + ) + + /// + /// + Procedure( + description: Option(String), + parameters: Option(XrpcParams), + input: Option(XrpcBody), + output: Option(XrpcBody), + errors: List(XrpcError), + ) + + /// + /// + Subscription( + description: Option(String), + parameters: Option(XrpcParams), + message: SubscriptionMessage, + errors: List(XrpcError), + ) + + /// + Schema(schema: Schema) + // PermissionSet +} + +/// +/// +pub type RecordKey { + TidKey + NsidKey + LiteralKey(value: String) + AnyKey +} + +/// +/// +pub type XrpcParams { + Params(required: Set(String), properties: Dict(String, Schema)) +} + +/// +/// +pub type XrpcBody { + XrpcBody( + description: Option(String), + encoding: String, + schema: Option(Schema), + ) +} + +/// +/// +pub type XrpcError { + XrpcError(name: String, description: Option(String)) +} + +/// +/// +pub type SubscriptionMessage { + SubscriptionMessage(description: Option(String), schema: Schema) +} + +// CONSTANTS ------------------------------------------------------------------- + +const definition_zero = Schema(schema.zero) + +// CONSTRUCTORS ---------------------------------------------------------------- + +/// +/// +pub fn decoder() -> Decoder(Lexicon) { + use description <- decoding.optional_field("description", decode.string) + use version <- decode.field("lexicon", { + decode.then(decode.int, fn(version) { + case version { + 1 -> decode.success(version) + _ -> decode.failure(1, "1") + } + }) + }) + + use id <- decode.field("id", nsid.decoder()) + use main <- decoding.optional_field("main", { + decode.one_of(definition_decoder(), [ + schema.decoder() |> decode.map(Schema), + ]) + }) + + use defs <- decode.field("defs", { + use keys <- decode.then(decode.dict(decode.string, decode.success(Nil))) + use defs, key, _ <- dict.fold(keys, decode.success(dict.new())) + use defs <- decode.then(defs) + + case key { + "main" -> decode.success(defs) + _ -> + decode.field(key, schema.decoder(), fn(def) { + decode.success(dict.insert(defs, key, def)) + }) + } + }) + + decode.success(Lexicon(version:, id:, description:, main:, defs:)) +} + +fn definition_decoder() -> Decoder(MainDefinition) { + use type_ <- decode.field( + "type", + decode.then(decode.string, fn(type_) { + case type_ { + "record" | "query" | "procedure" | "subscription" -> + decode.success(type_) + + _ -> + decode.failure( + type_, + "'record' | 'query' | 'procedure' | 'subscription'", + ) + } + }), + ) + + case type_ { + "record" -> record_definition_decoder() + "query" -> query_definition_decoder() + "procedure" -> procedure_definition_decoder() + "subscription" -> subscription_definition_decoder() + + _ -> decode.failure(definition_zero, "lexicon.MainDefinition") + } +} + +fn record_definition_decoder() -> Decoder(MainDefinition) { + use description <- decoding.optional_field("description", decode.string) + use key <- decode.field("key", record_key_decoder()) + use properties, required, nullable <- record_schema_decoder + + decode.success(Record(description:, key:, properties:, required:, nullable:)) +} + +fn record_key_decoder() -> Decoder(RecordKey) { + use string <- decode.then(decode.string) + + case string { + "tid" -> decode.success(TidKey) + "nsid" -> decode.success(NsidKey) + "literal:" <> value -> decode.success(LiteralKey(value)) + "any" -> decode.success(AnyKey) + _ -> decode.failure(AnyKey, "'tid' | 'nsid' | 'literal:' | 'any'") + } +} + +fn record_schema_decoder( + next: fn(Dict(String, Schema), Set(String), Set(String)) -> + Decoder(MainDefinition), +) -> Decoder(MainDefinition) { + use _ <- decode.field("type", { + decode.then(decode.string, fn(string) { + case string { + "object" -> decode.success(string) + _ -> decode.failure(string, "'object'") + } + }) + }) + + use properties <- decode.field("properties", { + decode.dict(decode.string, schema.decoder()) + }) + + use required <- decoding.optional_field("required", { + decode.list(decode.string) + }) + + use nullable <- decoding.optional_field("nullable", { + decode.list(decode.string) + }) + + next( + properties, + required + |> option.map(set.from_list) + |> option.lazy_unwrap(set.new), + nullable + |> option.map(set.from_list) + |> option.lazy_unwrap(set.new), + ) +} + +fn query_definition_decoder() -> Decoder(MainDefinition) { + use description <- decoding.optional_field("description", decode.string) + use parameters <- decoding.optional_field("parameters", xrpc_params_decoder()) + use output <- decoding.optional_field("output", xrpc_body_decoder()) + use errors <- decoding.optional_field( + "errors", + decode.list(xrpc_error_decoder()), + ) + + decode.success(Query( + description:, + parameters: parameters, + output: output, + errors: errors + |> option.lazy_unwrap(list.new), + )) +} + +fn procedure_definition_decoder() -> Decoder(MainDefinition) { + use description <- decoding.optional_field("description", decode.string) + use parameters <- decoding.optional_field("parameters", xrpc_params_decoder()) + use input <- decoding.optional_field("input", xrpc_body_decoder()) + use output <- decoding.optional_field("output", xrpc_body_decoder()) + use errors <- decoding.optional_field( + "errors", + decode.list(xrpc_error_decoder()), + ) + + decode.success(Procedure( + description:, + parameters: parameters, + input: input, + output: output, + errors: errors + |> option.lazy_unwrap(list.new), + )) +} + +fn xrpc_params_decoder() -> Decoder(XrpcParams) { + use required <- decoding.optional_field( + "required", + decode.list(decode.string), + ) + use properties <- decode.field( + "properties", + decode.dict(decode.string, schema.decoder()), + ) + + decode.success(Params( + required: required + |> option.map(set.from_list) + |> option.lazy_unwrap(set.new), + properties:, + )) +} + +fn xrpc_body_decoder() -> Decoder(XrpcBody) { + use description <- decoding.optional_field("description", decode.string) + use encoding <- decode.field("encoding", decode.string) + use schema <- decoding.optional_field("schema", schema.decoder()) + + decode.success(XrpcBody(description:, encoding:, schema:)) +} + +fn xrpc_error_decoder() -> Decoder(XrpcError) { + use name <- decode.field("name", decode.string) + use description <- decoding.optional_field("description", decode.string) + + decode.success(XrpcError(name:, description:)) +} + +fn subscription_definition_decoder() -> Decoder(MainDefinition) { + use description <- decoding.optional_field("description", decode.string) + use parameters <- decoding.optional_field("parameters", xrpc_params_decoder()) + use message <- decode.field("message", subscription_message_decoder()) + use errors <- decoding.optional_field( + "errors", + decode.list(xrpc_error_decoder()), + ) + + decode.success(Subscription( + description:, + parameters: parameters, + message: message, + errors: errors + |> option.lazy_unwrap(list.new), + )) +} + +fn subscription_message_decoder() -> Decoder(SubscriptionMessage) { + use description <- decoding.optional_field("description", decode.string) + use schema <- decode.field("schema", schema.decoder()) + + decode.success(SubscriptionMessage(description:, schema:)) +} + +// QUERIES --------------------------------------------------------------------- + +pub fn validate( + value: Dynamic, + against schema: Schema, + from lexicon: Lexicon, + with references: Dict(Nsid, Lexicon), +) -> Result(Nil, Nil) { + case decode.run(value, do_validate(schema, lexicon, references)) { + Ok(_) -> Ok(Nil) + Error(_) -> Error(Nil) + } +} + +fn do_validate( + schema: Schema, + this: Lexicon, + references: Dict(Nsid, Lexicon), +) -> Decoder(Nil) { + case schema { + schema.Boolean(constant:, ..) -> { + use bool <- decode.then(decode.bool) + use <- do_validate_constraint(constant, fn(constant) { + case bool == constant { + True -> Ok(Nil) + False -> Error(bool.to_string(constant)) + } + }) + + decode.success(Nil) + } + + schema.Integer(minimum:, maximum:, enum:, constant:, ..) -> { + use int <- decode.then(decode.int) + use <- do_validate_constraint(minimum, fn(minimum) { + case int >= minimum { + True -> Ok(Nil) + False -> Error(">= " <> int.to_string(minimum)) + } + }) + + use <- do_validate_constraint(maximum, fn(maximum) { + case int <= maximum { + True -> Ok(Nil) + False -> Error("<= " <> int.to_string(maximum)) + } + }) + + use <- do_validate_constraint(enum, fn(enum) { + case set.contains(enum, int) { + True -> Ok(Nil) + False -> + Error( + enum + |> set.to_list + |> list.map(int.to_string) + |> string.join(" | "), + ) + } + }) + + use <- do_validate_constraint(constant, fn(constant) { + case int == constant { + True -> Ok(Nil) + False -> Error(int.to_string(constant)) + } + }) + + decode.success(Nil) + } + + schema.String( + format:, + min_length:, + max_length:, + min_graphemes:, + max_graphemes:, + known_values:, + enum:, + constant:, + .., + ) -> { + use string <- decode.then(decode.string) + use <- do_validate_constraint(format, validate_string_format(string, _)) + + let bytes = bit_array.from_string(string) + let bytes_size = bit_array.byte_size(bytes) + + use <- do_validate_constraint(min_length, fn(min_length) { + case bytes_size >= min_length { + True -> Ok(Nil) + False -> Error(">= " <> int.to_string(min_length) <> " bytes") + } + }) + + use <- do_validate_constraint(max_length, fn(max_length) { + case bytes_size <= max_length { + True -> Ok(Nil) + False -> Error("<= " <> int.to_string(max_length) <> " bytes") + } + }) + + let length = string.length(string) + + use <- do_validate_constraint(min_graphemes, fn(min_graphemes) { + case length >= min_graphemes { + True -> Ok(Nil) + False -> Error(">= " <> int.to_string(min_graphemes) <> " graphemes") + } + }) + + use <- do_validate_constraint(max_graphemes, fn(max_graphemes) { + case length <= max_graphemes { + True -> Ok(Nil) + False -> Error("<= " <> int.to_string(max_graphemes) <> " graphemes") + } + }) + + use <- do_validate_constraint(known_values, fn(known_values) { + case set.contains(known_values, string) { + True -> Ok(Nil) + False -> + Error( + known_values + |> set.to_list + |> string.join(" | "), + ) + } + }) + + use <- do_validate_constraint(enum, fn(enum) { + case set.contains(enum, string) { + True -> Ok(Nil) + False -> + Error( + enum + |> set.to_list + |> string.join(" | "), + ) + } + }) + + use <- do_validate_constraint(constant, fn(constant) { + case string == constant { + True -> Ok(Nil) + False -> Error(constant) + } + }) + + decode.success(Nil) + } + + schema.Bytes(min_length:, max_length:, ..) -> { + use bytes <- decode.then(decode.bit_array) + let bytes_size = bit_array.byte_size(bytes) + + use <- do_validate_constraint(min_length, fn(min_length) { + case bytes_size >= min_length { + True -> Ok(Nil) + False -> Error(">= " <> int.to_string(min_length) <> " bytes") + } + }) + + use <- do_validate_constraint(max_length, fn(max_length) { + case bytes_size <= max_length { + True -> Ok(Nil) + False -> Error("<= " <> int.to_string(max_length) <> " bytes") + } + }) + + decode.success(Nil) + } + + schema.CidLink(..) -> { + use _ <- decode.then(cid.decoder()) + + decode.success(Nil) + } + + schema.Array(items:, min_length:, max_length:, ..) -> { + use list <- decode.then(decode.list(do_validate(items, this, references))) + + let length = list.length(list) + + use <- do_validate_constraint(min_length, fn(min_length) { + case length >= min_length { + True -> Ok(Nil) + False -> Error(">= " <> int.to_string(min_length) <> " items") + } + }) + + use <- do_validate_constraint(max_length, fn(max_length) { + case length <= max_length { + True -> Ok(Nil) + False -> Error("<= " <> int.to_string(max_length) <> " items") + } + }) + + decode.success(Nil) + } + + schema.Object(properties:, required:, nullable:, ..) -> { + use prev, key, schema <- dict.fold(properties, decode.success(Nil)) + use _ <- decode.then(prev) + + case set.contains(required, key), set.contains(nullable, key) { + True, True -> + decode.optional(do_validate(schema, this, references)) + |> decode.at([key], _) + |> decode.map(option.unwrap(_, Nil)) + + True, False -> decode.at([key], do_validate(schema, this, references)) + + False, True -> + decode.optional(do_validate(schema, this, references)) + |> decode.optionally_at([key], None, _) + |> decode.map(option.unwrap(_, Nil)) + + False, False -> + decode.optionally_at([key], Nil, { + do_validate(schema, this, references) + }) + } + } + + schema.Blob(accept:, max_size:, ..) -> { + use _ <- decode.field( + "$type", + decode.then(decode.string, fn(string) { + case string { + "blob" -> decode.success(Nil) + _ -> decode.failure(Nil, "'blob'") + } + }), + ) + + use _ <- decode.field("mimeType", { + use mime_type <- decode.then(decode.string) + use <- do_validate_constraint(accept, fn(accept) { + case set.contains(accept, mime_type) { + True -> Ok(Nil) + False -> + Error( + accept + |> set.to_list + |> string.join(" | "), + ) + } + }) + + decode.success(Nil) + }) + + use _ <- decode.field("size", { + use size <- decode.then(decode.int) + use <- do_validate_constraint(max_size, fn(max_size) { + case size <= max_size { + True -> Ok(Nil) + False -> Error("<= " <> int.to_string(max_size) <> " bytes") + } + }) + + decode.success(Nil) + }) + + decode.success(Nil) + } + + schema.Ref(ref:, ..) -> do_validate_ref(ref, this, references) + + schema.Union(refs:, closed:, ..) -> { + let decoders = + set.fold(refs, [], fn(decoders, ref) { + [do_validate_ref(ref, this, references), ..decoders] + }) + + case decoders { + [] if closed -> decode.failure(Nil, "") + [] -> decode.success(Nil) + [first, ..rest] -> + decode.one_of(first, list.reverse([decode.success(Nil), ..rest])) + } + } + + schema.Unknown(..) -> decode.success(Nil) + + schema.Token(..) -> decode.success(Nil) + } +} + +fn do_validate_ref( + ref: schema.Reference, + this: Lexicon, + references: Dict(Nsid, Lexicon), +) -> Decoder(Nil) { + case ref { + schema.Qualified(nsid:, def:) -> + case dict.get(references, nsid) { + Ok(lexicon) -> + case dict.get(lexicon.defs, option.unwrap(def, "main")) { + Ok(schema) -> do_validate(schema, lexicon, references) + Error(_) -> decode.failure(Nil, "") + } + + Error(_) -> decode.failure(Nil, "") + } + + schema.Unqualified(def:) -> + case dict.get(this.defs, def) { + Ok(schema) -> do_validate(schema, this, references) + Error(_) -> decode.failure(Nil, "") + } + } +} + +fn do_validate_constraint( + constraint: Option(b), + check: fn(b) -> Result(Nil, String), + next: fn() -> Decoder(Nil), +) -> Decoder(Nil) { + case constraint { + Some(b) -> + case check(b) { + Ok(_) -> next() + Error(message) -> decode.failure(Nil, message) + } + None -> next() + } +} + +fn validate_string_format( + value: String, + format: schema.StringFormat, +) -> Result(Nil, String) { + case format { + schema.AtIdentifier -> + case did.from_string(value) { + Ok(_) -> Ok(Nil) + Error(_) -> + case handle.from_string(value) { + Ok(_) -> Ok(Nil) + Error(_) -> Error("Did | Handle") + } + } + + schema.AtUri -> + case at_uri.from_string(value) { + Ok(_) -> Ok(Nil) + Error(_) -> Error("AtUri") + } + + schema.Cid -> + case cid.from_string(value) { + Ok(_) -> Ok(Nil) + Error(_) -> Error("Cid") + } + + schema.Datetime -> + case timestamp.parse_rfc3339(value) { + Ok(_) -> Ok(Nil) + Error(_) -> Error("Datetime") + } + + schema.Did -> + case did.from_string(value) { + Ok(_) -> Ok(Nil) + Error(_) -> Error("Did") + } + + schema.Handle -> + case handle.from_string(value) { + Ok(_) -> Ok(Nil) + Error(_) -> Error("Handle") + } + + schema.Nsid -> + case nsid.from_string(value) { + Ok(_) -> Ok(Nil) + Error(_) -> Error("Nsid") + } + + schema.Tid -> + case tid.from_string(value) { + Ok(_) -> Ok(Nil) + Error(_) -> Error("Tid") + } + + schema.RecordKey -> + case rkey.from_string(value) { + Ok(_) -> Ok(Nil) + Error(_) -> Error("RecordKey") + } + + schema.Uri -> + case uri.parse(value) { + Ok(_) -> Ok(Nil) + Error(_) -> Error("Uri") + } + + schema.Language -> Ok(Nil) + } +} + +pub fn dependencies(lexicon: Lexicon) -> Set(Nsid) { + let main_dependencies = case lexicon.main { + Some(Record(properties:, ..)) -> + do_record_dependencies(set.new(), properties) + + Some(Query(parameters:, output:, ..)) -> + set.new() + |> do_xrpc_param_dependencies(parameters) + |> do_xrpc_body_dependencies(output) + + Some(Procedure(parameters:, input:, output:, ..)) -> + set.new() + |> do_xrpc_param_dependencies(parameters) + |> do_xrpc_body_dependencies(input) + |> do_xrpc_body_dependencies(output) + + Some(Subscription(parameters:, message:, ..)) -> + schema.dependencies(message.schema) + |> do_xrpc_param_dependencies(parameters) + + Some(Schema(schema:)) -> schema.dependencies(schema) + + None -> set.new() + } + + use dependencies, _, definition <- dict.fold(lexicon.defs, main_dependencies) + + schema.dependencies(definition) + |> set.union(dependencies) +} + +fn do_record_dependencies( + dependencies: Set(Nsid), + properties: Dict(String, Schema), +) -> Set(Nsid) { + use dependencies, _, schema <- dict.fold(properties, dependencies) + + schema.dependencies(schema) + |> set.union(dependencies) +} + +fn do_xrpc_param_dependencies( + dependencies: Set(Nsid), + params: Option(XrpcParams), +) -> Set(Nsid) { + case params { + Some(Params(properties:, ..)) -> { + use dependencies, _, schema <- dict.fold(properties, dependencies) + + schema.dependencies(schema) + |> set.union(dependencies) + } + + None -> dependencies + } +} + +fn do_xrpc_body_dependencies( + dependencies: Set(Nsid), + body: Option(XrpcBody), +) -> Set(Nsid) { + case body { + Some(XrpcBody(schema: Some(schema), ..)) -> + schema.dependencies(schema) + |> set.union(dependencies) + Some(_) | None -> dependencies + } +} diff --git a/src/at/lexicon/schema.gleam b/src/at/lexicon/schema.gleam new file mode 100644 index 0000000..c28c003 --- /dev/null +++ b/src/at/lexicon/schema.gleam @@ -0,0 +1,384 @@ +// IMPORTS --------------------------------------------------------------------- + +import at/internal/decoding +import at/nsid.{type Nsid} +import gleam/dict.{type Dict} +import gleam/dynamic/decode.{type Decoder} +import gleam/option.{type Option, None, Some} +import gleam/set.{type Set} +import gleam/string + +// TYPES ----------------------------------------------------------------------- + +pub type Schema { + Boolean( + description: Option(String), + default: Option(Bool), + constant: Option(Bool), + ) + + Integer( + description: Option(String), + minimum: Option(Int), + maximum: Option(Int), + enum: Option(Set(Int)), + default: Option(Int), + constant: Option(Int), + ) + + String( + description: Option(String), + format: Option(StringFormat), + min_length: Option(Int), + max_length: Option(Int), + min_graphemes: Option(Int), + max_graphemes: Option(Int), + known_values: Option(Set(String)), + enum: Option(Set(String)), + default: Option(String), + constant: Option(String), + ) + + Bytes( + description: Option(String), + min_length: Option(Int), + max_length: Option(Int), + ) + + CidLink(description: Option(String)) + + Array( + description: Option(String), + items: Schema, + min_length: Option(Int), + max_length: Option(Int), + ) + + Object( + description: Option(String), + properties: Dict(String, Schema), + required: Set(String), + nullable: Set(String), + ) + + Blob( + description: Option(String), + accept: Option(Set(String)), + max_size: Option(Int), + ) + + Ref(description: Option(String), ref: Reference) + + Union(description: Option(String), refs: Set(Reference), closed: Bool) + + Unknown(description: Option(String)) + + Token(description: Option(String)) +} + +/// +/// +pub type StringFormat { + AtIdentifier + AtUri + Cid + Datetime + Did + Handle + Nsid + Tid + RecordKey + Uri + Language +} + +/// +/// +pub type Reference { + Qualified(nsid: Nsid, def: Option(String)) + Unqualified(def: String) +} + +// CONSTANTS ------------------------------------------------------------------- + +@internal +pub const zero = Unknown(description: None) + +// CONSTRUCTORS ---------------------------------------------------------------- + +/// +/// +pub fn decoder() -> Decoder(Schema) { + use type_ <- decode.field( + "type", + decode.then(decode.string, fn(string) { + case string { + "boolean" + | "integer" + | "string" + | "bytes" + | "cid-link" + | "array" + | "object" + | "blob" + | "ref" + | "union" + | "unknown" + | "token" -> decode.success(string) + + _ -> + decode.failure( + string, + "'boolean' | 'integer' | 'string' | 'bytes' | 'cid-link' | 'array' | 'object' | 'blob' | 'ref' | 'union' | 'unknown' | 'token'", + ) + } + }), + ) + + case type_ { + "boolean" -> boolean_decoder() + "integer" -> integer_decoder() + "string" -> string_decoder() + "bytes" -> bytes_decoder() + "cid-link" -> cid_link_decoder() + "array" -> array_decoder() + "object" -> object_decoder() + "blob" -> blob_decoder() + "ref" -> ref_decoder() + "union" -> union_decoder() + "unknown" -> unknown_decoder() + "token" -> token_decoder() + + _ -> decode.failure(zero, "schema.Schema") + } +} + +fn boolean_decoder() -> Decoder(Schema) { + use description <- decoding.optional_field("description", decode.string) + use default <- decoding.optional_field("default", decode.bool) + use constant <- decoding.optional_field("constant", decode.bool) + + decode.success(Boolean(description:, default:, constant:)) +} + +fn integer_decoder() -> Decoder(Schema) { + use description <- decoding.optional_field("description", decode.string) + use minimum <- decoding.optional_field("minimum", decode.int) + use maximum <- decoding.optional_field("maximum", decode.int) + use enum <- decoding.optional_field("enum", decode.list(decode.int)) + use default <- decoding.optional_field("default", decode.int) + use constant <- decoding.optional_field("constant", decode.int) + + decode.success(Integer( + description:, + minimum:, + maximum:, + enum: option.map(enum, set.from_list), + default:, + constant:, + )) +} + +fn string_decoder() -> Decoder(Schema) { + use description <- decoding.optional_field("description", decode.string) + use format <- decoding.optional_field("format", string_format_decoder()) + use min_length <- decoding.optional_field("minLength", decode.int) + use max_length <- decoding.optional_field("maxLength", decode.int) + use min_graphemes <- decoding.optional_field("minGraphemes", decode.int) + use max_graphemes <- decoding.optional_field("maxGraphemes", decode.int) + use known_values <- decoding.optional_field( + "knownValues", + decode.list(decode.string), + ) + use enum <- decoding.optional_field("enum", decode.list(decode.string)) + use default <- decoding.optional_field("default", decode.string) + use constant <- decoding.optional_field("constant", decode.string) + + decode.success(String( + description:, + format:, + min_length:, + max_length:, + min_graphemes:, + max_graphemes:, + known_values: option.map(known_values, set.from_list), + enum: option.map(enum, set.from_list), + default:, + constant:, + )) +} + +fn string_format_decoder() -> Decoder(StringFormat) { + use string <- decode.then(decode.string) + + case string { + "at-identifier" -> decode.success(AtIdentifier) + "at-uri" -> decode.success(AtUri) + "cid" -> decode.success(Cid) + "datetime" -> decode.success(Datetime) + "did" -> decode.success(Did) + "handle" -> decode.success(Handle) + "nsid" -> decode.success(Nsid) + "tid" -> decode.success(Tid) + "record-key" -> decode.success(RecordKey) + "uri" -> decode.success(Uri) + "language" -> decode.success(Language) + + _ -> + decode.failure( + AtIdentifier, + "'at-identifier' | 'at-uri' | 'cid' | 'datetime' | 'did' | 'handle' | 'nsid' | 'tid' | 'record-key' | 'uri' | 'language'", + ) + } +} + +@internal +pub fn bytes_decoder() -> Decoder(Schema) { + use description <- decoding.optional_field("description", decode.string) + use min_length <- decoding.optional_field("minLength", decode.int) + use max_length <- decoding.optional_field("maxLength", decode.int) + + decode.success(Bytes(description:, min_length:, max_length:)) +} + +fn cid_link_decoder() -> Decoder(Schema) { + use description <- decoding.optional_field("description", decode.string) + + decode.success(CidLink(description:)) +} + +fn array_decoder() -> Decoder(Schema) { + use description <- decoding.optional_field("description", decode.string) + use items <- decode.field("items", decoder()) + use min_length <- decoding.optional_field("minLength", decode.int) + use max_length <- decoding.optional_field("maxLength", decode.int) + + decode.success(Array(description:, items:, min_length:, max_length:)) +} + +fn object_decoder() -> Decoder(Schema) { + use description <- decoding.optional_field("description", decode.string) + use properties <- decode.field( + "properties", + decode.dict(decode.string, decoder()), + ) + use required <- decoding.optional_field( + "required", + decode.list(decode.string), + ) + use nullable <- decoding.optional_field( + "nullable", + decode.list(decode.string), + ) + + decode.success(Object( + description:, + properties:, + required: required + |> option.map(set.from_list) + |> option.lazy_unwrap(set.new), + nullable: nullable + |> option.map(set.from_list) + |> option.lazy_unwrap(set.new), + )) +} + +fn blob_decoder() -> Decoder(Schema) { + use description <- decoding.optional_field("description", decode.string) + use accept <- decoding.optional_field("accept", decode.list(decode.string)) + use max_size <- decoding.optional_field("maxSize", decode.int) + + decode.success(Blob( + description:, + accept: accept |> option.map(set.from_list), + max_size:, + )) +} + +fn ref_decoder() -> Decoder(Schema) { + use description <- decoding.optional_field("description", decode.string) + use ref <- decode.field("ref", reference_decoder()) + + decode.success(Ref(description:, ref:)) +} + +fn union_decoder() -> Decoder(Schema) { + use description <- decoding.optional_field("description", decode.string) + use refs <- decode.field("refs", decode.list(reference_decoder())) + use closed <- decoding.optional_field("closed", decode.bool) + + decode.success(Union( + description:, + refs: refs + |> set.from_list, + closed: option.unwrap(closed, False), + )) +} + +fn unknown_decoder() -> Decoder(Schema) { + use description <- decoding.optional_field("description", decode.string) + + decode.success(Unknown(description:)) +} + +fn token_decoder() -> Decoder(Schema) { + use description <- decoding.optional_field("description", decode.string) + + decode.success(Token(description:)) +} + +fn reference_decoder() -> Decoder(Reference) { + use string <- decode.then(decode.string) + + case string.split_once(string, "#") { + Ok(#("", fragment)) -> decode.success(Unqualified(fragment)) + + Ok(#(nsid, fragment)) -> + case nsid.from_string(nsid) { + Ok(nsid) -> decode.success(Qualified(nsid:, def: Some(fragment))) + Error(_) -> decode.failure(Unqualified(fragment), "Nsid") + } + + Error(_) -> + case nsid.from_string(string) { + Ok(nsid) -> decode.success(Qualified(nsid:, def: None)) + Error(_) -> decode.failure(Unqualified(string), "Nsid") + } + } +} + +// QUERIES --------------------------------------------------------------------- + +/// +/// +pub fn dependencies(schema: Schema) -> Set(Nsid) { + do_dependencies(schema, set.new()) +} + +fn do_dependencies(schema: Schema, dependencies: Set(Nsid)) -> Set(Nsid) { + case schema { + Array(items:, ..) -> do_dependencies(items, dependencies) + + Object(properties:, ..) -> { + use dependencies, _, property <- dict.fold(properties, dependencies) + do_dependencies(property, dependencies) + } + + Ref(ref:, ..) -> + case ref { + Qualified(nsid:, ..) -> set.insert(dependencies, nsid) + Unqualified(..) -> dependencies + } + + Union(refs:, ..) -> { + use dependencies, ref <- set.fold(refs, dependencies) + + case ref { + Qualified(nsid:, ..) -> set.insert(dependencies, nsid) + Unqualified(..) -> dependencies + } + } + + _ -> dependencies + } +}