///|
fn uri_json(uri : @reference.UriReference) -> @exact.Value {
  @exact.Value::string(uri.text())
}

///|
fn[T] inline_json(
  value : InlineOrReference[T],
  encode : (T) -> @exact.Value,
) -> @exact.Value {
  match value {
    Inline(value) => encode(value)
    Reference(ref_) =>
      @exact.Value::object([("$ref", @exact.Value::string(ref_.text()))]).unwrap()
  }
}

///|
fn[T] add_optional(
  fields : Array[(String, @exact.Value)],
  name : String,
  value : T?,
  encode : (T) -> @exact.Value,
) -> Unit {
  if value is Some(value) {
    fields.push((name, encode(value)))
  }
}

///|
fn prefix_diagnostic(name : String, d : @exact.Diagnostic) -> @exact.Diagnostic {
  let path = [name]
  for part in d.path() {
    path.push(part)
  }
  @exact.Diagnostic::new(
    d.code(),
    d.message(),
    path~,
    span=d.span(),
    related=d.related(),
  )
}

///|
fn[T] add_dictionary(
  fields : Array[(String, @exact.Value)],
  name : String,
  entries : Array[(String, T)]?,
  encode : (T) -> @exact.Value,
) -> Result[Unit, @exact.Diagnostic] {
  if entries is Some(entries) {
    match
      @exact.Value::object(entries.map(entry => (entry.0, encode(entry.1)))) {
      Ok(value) => fields.push((name, value))
      Err(d) => return Err(prefix_diagnostic(name, d))
    }
  }
  Ok(())
}

///|
fn make_object(
  fields : Array[(String, @exact.Value)],
  kind : ObjectKind,
  extensions : Extensions,
  additional_fields : Array[(String, @exact.Value)],
) -> Result[@exact.Value, @exact.Diagnostic] {
  let known = field_names(kind)
  for (name, value) in additional_fields {
    if known.contains(name) || name.has_prefix("x-") {
      return Err(
        @exact.Diagnostic::new(
          @exact.WrongType,
          "Known fields and extensions require their typed inputs",
          path=[name],
        ),
      )
    }
    fields.push((name, value))
  }
  for (name, value) in extensions.entries() {
    fields.push((name.to_string(), value))
  }
  @exact.Value::object(fields)
}

///|
pub fn Contact::new(
  name~ : String? = None,
  url~ : @reference.UriReference? = None,
  email~ : String? = None,
  extensions~ : Extensions = Extensions::empty(),
  additional_fields~ : Array[(String, @exact.Value)] = [],
  limits~ : @exact.Limits = @exact.Limits::new(),
) -> Result[Contact, @exact.Diagnostic] {
  let fields : Array[(String, @exact.Value)] = []
  add_optional(fields, "name", name, @exact.Value::string)
  add_optional(fields, "url", url, uri_json)
  add_optional(fields, "email", email, @exact.Value::string)
  match make_object(fields, ContactObject, extensions, additional_fields) {
    Ok(value) => Contact::from_json(value, limits~)
    Err(d) => Err(d)
  }
}

///|
pub fn License::new(
  name~ : String? = None,
  url~ : @reference.UriReference? = None,
  extensions~ : Extensions = Extensions::empty(),
  additional_fields~ : Array[(String, @exact.Value)] = [],
  limits~ : @exact.Limits = @exact.Limits::new(),
) -> Result[License, @exact.Diagnostic] {
  let fields : Array[(String, @exact.Value)] = []
  add_optional(fields, "name", name, @exact.Value::string)
  add_optional(fields, "url", url, uri_json)
  match make_object(fields, LicenseObject, extensions, additional_fields) {
    Ok(value) => License::from_json(value, limits~)
    Err(d) => Err(d)
  }
}

///|
pub fn ExternalDocumentation::new(
  url : @reference.UriReference,
  description~ : String? = None,
  extensions~ : Extensions = Extensions::empty(),
  additional_fields~ : Array[(String, @exact.Value)] = [],
  limits~ : @exact.Limits = @exact.Limits::new(),
) -> Result[ExternalDocumentation, @exact.Diagnostic] {
  let fields : Array[(String, @exact.Value)] = []
  fields.push(("url", uri_json(url)))
  add_optional(fields, "description", description, @exact.Value::string)
  match
    make_object(
      fields,
      ExternalDocumentationObject,
      extensions,
      additional_fields,
    ) {
    Ok(value) => ExternalDocumentation::from_json(value, limits~)
    Err(d) => Err(d)
  }
}

///|
pub fn ServerVariable::new(
  default_value : String,
  enum_values~ : Array[String]? = None,
  description~ : String? = None,
  extensions~ : Extensions = Extensions::empty(),
  additional_fields~ : Array[(String, @exact.Value)] = [],
  limits~ : @exact.Limits = @exact.Limits::new(),
) -> Result[ServerVariable, @exact.Diagnostic] {
  let fields : Array[(String, @exact.Value)] = []
  fields.push(("default", @exact.Value::string(default_value)))
  add_optional(fields, "enum", enum_values, fn(v) {
    @exact.Value::array(v.map(name => @exact.Value::string(name)))
  })
  add_optional(fields, "description", description, @exact.Value::string)
  match
    make_object(fields, ServerVariableObject, extensions, additional_fields) {
    Ok(value) => ServerVariable::from_json(value, limits~)
    Err(d) => Err(d)
  }
}

///|
pub fn Server::new(
  url : String,
  name~ : String? = None,
  summary~ : String? = None,
  description~ : String? = None,
  variables~ : Array[(String, ServerVariable)]? = None,
  extensions~ : Extensions = Extensions::empty(),
  additional_fields~ : Array[(String, @exact.Value)] = [],
  limits~ : @exact.Limits = @exact.Limits::new(),
) -> Result[Server, @exact.Diagnostic] {
  let fields : Array[(String, @exact.Value)] = []
  fields.push(("url", @exact.Value::string(url)))
  add_optional(fields, "name", name, @exact.Value::string)
  add_optional(fields, "summary", summary, @exact.Value::string)
  add_optional(fields, "description", description, @exact.Value::string)
  match
    add_dictionary(fields, "variables", variables, ServerVariable::to_json) {
    Ok(_) => ()
    Err(d) => return Err(d)
  }
  match make_object(fields, ServerObject, extensions, additional_fields) {
    Ok(value) => Server::from_json(value, limits~)
    Err(d) => Err(d)
  }
}

///|
pub fn Tag::new(
  name : String,
  summary~ : String? = None,
  description~ : String? = None,
  external_docs~ : ExternalDocumentation? = None,
  extensions~ : Extensions = Extensions::empty(),
  additional_fields~ : Array[(String, @exact.Value)] = [],
  limits~ : @exact.Limits = @exact.Limits::new(),
) -> Result[Tag, @exact.Diagnostic] {
  let fields : Array[(String, @exact.Value)] = []
  fields.push(("name", @exact.Value::string(name)))
  add_optional(fields, "summary", summary, @exact.Value::string)
  add_optional(fields, "description", description, @exact.Value::string)
  add_optional(
    fields,
    "externalDocs",
    external_docs,
    ExternalDocumentation::to_json,
  )
  match make_object(fields, TagObject, extensions, additional_fields) {
    Ok(value) => Tag::from_json(value, limits~)
    Err(d) => Err(d)
  }
}

///|
pub fn ErrorObject::new(
  code : ErrorCode,
  message : String,
  data~ : @exact.Value? = None,
  extensions~ : Extensions = Extensions::empty(),
  additional_fields~ : Array[(String, @exact.Value)] = [],
  limits~ : @exact.Limits = @exact.Limits::new(),
) -> Result[ErrorObject, @exact.Diagnostic] {
  let fields : Array[(String, @exact.Value)] = []
  fields.push(("code", @exact.Value::number(code.number())))
  fields.push(("message", @exact.Value::string(message)))
  add_optional(fields, "data", data, fn(v) { v })
  match make_object(fields, ErrorObjectKind, extensions, additional_fields) {
    Ok(value) => ErrorObject::from_json(value, limits~)
    Err(d) => Err(d)
  }
}

///|
pub fn Example::new(
  payload : ExampleValue,
  name~ : String? = None,
  summary~ : String? = None,
  description~ : String? = None,
  extensions~ : Extensions = Extensions::empty(),
  additional_fields~ : Array[(String, @exact.Value)] = [],
  limits~ : @exact.Limits = @exact.Limits::new(),
) -> Result[Example, @exact.Diagnostic] {
  let fields : Array[(String, @exact.Value)] = []
  match payload {
    NoExampleValue => ()
    LiteralExample(value) => fields.push(("value", value))
    ExternalExample(uri) => fields.push(("externalValue", uri_json(uri)))
  }
  add_optional(fields, "name", name, @exact.Value::string)
  add_optional(fields, "summary", summary, @exact.Value::string)
  add_optional(fields, "description", description, @exact.Value::string)
  match make_object(fields, ExampleObject, extensions, additional_fields) {
    Ok(value) => Example::from_json(value, limits~)
    Err(d) => Err(d)
  }
}

///|
pub fn ExamplePairing::new(
  name : String,
  params : Array[InlineOrReference[Example]],
  summary~ : String? = None,
  description~ : String? = None,
  result~ : InlineOrReference[Example]? = None,
  extensions~ : Extensions = Extensions::empty(),
  additional_fields~ : Array[(String, @exact.Value)] = [],
  limits~ : @exact.Limits = @exact.Limits::new(),
) -> Result[ExamplePairing, @exact.Diagnostic] {
  let fields : Array[(String, @exact.Value)] = []
  fields.push(("name", @exact.Value::string(name)))
  fields.push(
    (
      "params",
      @exact.Value::array(
        params.map(item => inline_json(item, Example::to_json)),
      ),
    ),
  )
  add_optional(fields, "summary", summary, @exact.Value::string)
  add_optional(fields, "description", description, @exact.Value::string)
  add_optional(fields, "result", result, fn(v) {
    inline_json(v, Example::to_json)
  })
  match
    make_object(fields, ExamplePairingObject, extensions, additional_fields) {
    Ok(value) => ExamplePairing::from_json(value, limits~)
    Err(d) => Err(d)
  }
}

///|
pub fn Link::new(
  name~ : String? = None,
  summary~ : String? = None,
  description~ : String? = None,
  target_method~ : MethodName? = None,
  params~ : Array[(String, @exact.Value)]? = None,
  server~ : Server? = None,
  extensions~ : Extensions = Extensions::empty(),
  additional_fields~ : Array[(String, @exact.Value)] = [],
  limits~ : @exact.Limits = @exact.Limits::new(),
) -> Result[Link, @exact.Diagnostic] {
  let fields : Array[(String, @exact.Value)] = []
  add_optional(fields, "name", name, @exact.Value::string)
  add_optional(fields, "summary", summary, @exact.Value::string)
  add_optional(fields, "description", description, @exact.Value::string)
  add_optional(fields, "method", target_method, fn(v) {
    @exact.Value::string(v.to_string())
  })
  match add_dictionary(fields, "params", params, fn(v) { v }) {
    Ok(_) => ()
    Err(d) => return Err(d)
  }
  add_optional(fields, "server", server, Server::to_json)
  match make_object(fields, LinkObject, extensions, additional_fields) {
    Ok(value) => Link::from_json(value, limits~)
    Err(d) => Err(d)
  }
}

///|
pub fn Info::new(
  title : String,
  version : String,
  description~ : String? = None,
  terms_of_service~ : @reference.UriReference? = None,
  contact~ : Contact? = None,
  license~ : License? = None,
  extensions~ : Extensions = Extensions::empty(),
  additional_fields~ : Array[(String, @exact.Value)] = [],
  limits~ : @exact.Limits = @exact.Limits::new(),
) -> Result[Info, @exact.Diagnostic] {
  let fields : Array[(String, @exact.Value)] = []
  fields.push(("title", @exact.Value::string(title)))
  fields.push(("version", @exact.Value::string(version)))
  add_optional(fields, "description", description, @exact.Value::string)
  add_optional(fields, "termsOfService", terms_of_service, uri_json)
  add_optional(fields, "contact", contact, Contact::to_json)
  add_optional(fields, "license", license, License::to_json)
  match make_object(fields, InfoObject, extensions, additional_fields) {
    Ok(value) => Info::from_json(value, limits~)
    Err(d) => Err(d)
  }
}

///|
pub fn ContentDescriptor::new(
  name : String,
  schema : @schema.Schema,
  summary~ : String? = None,
  description~ : String? = None,
  required~ : Bool? = None,
  deprecated~ : Bool? = None,
  extensions~ : Extensions = Extensions::empty(),
  additional_fields~ : Array[(String, @exact.Value)] = [],
  limits~ : @exact.Limits = @exact.Limits::new(),
) -> Result[ContentDescriptor, @exact.Diagnostic] {
  let fields : Array[(String, @exact.Value)] = []
  fields.push(("name", @exact.Value::string(name)))
  fields.push(("schema", @schema.Schema::to_json(schema)))
  add_optional(fields, "summary", summary, @exact.Value::string)
  add_optional(fields, "description", description, @exact.Value::string)
  add_optional(fields, "required", required, @exact.Value::boolean)
  add_optional(fields, "deprecated", deprecated, @exact.Value::boolean)
  match
    make_object(fields, ContentDescriptorObject, extensions, additional_fields) {
    Ok(value) => ContentDescriptor::from_json(value, limits~)
    Err(d) => Err(d)
  }
}

///|
pub fn ContentDescriptor::from_json(
  value : @exact.Value,
  limits~ : @exact.Limits = @exact.Limits::new(),
) -> Result[ContentDescriptor, @exact.Diagnostic] {
  checked_object(value, limits, (v, path, limits) => {
    match descriptor(v, path, limits~) {
      Inline(value) => value
      Reference(_) =>
        raise DecodeError(
          @exact.Diagnostic::new(
            @exact.WrongType,
            "Expected an inline content descriptor",
            path~,
          ),
        )
    }
  })
}