// Structs that describe their own schema, and an app whose request and response
// bodies show up fully typed in `openapi.json`. This is the shape mctl generates
// from a spec: `derive(ToJson, FromJson)` for the wire, and a plain `T::schema()`
// that needs no instance, as FastAPI refers to the model class.

///|
/// A nested value type, to show a `$ref` chain: `User.address` references it
/// under `components/schemas`.
#derive(ToJson)
struct Address {
  city : String
  zip : String
}

///|
fn Address::schema() -> @moonapi.Schema {
  @moonapi.Schema::object("Address", [
    @moonapi.Field::new("city", SStr),
    @moonapi.Field::new("zip", SStr),
  ])
}

///|
/// The request body of `POST /users`. `age` is optional.
#derive(FromJson)
struct NewUser {
  name : String
  email : String
  age : Int32?
}

///|
fn NewUser::schema() -> @moonapi.Schema {
  @moonapi.Schema::object("NewUser", [
    @moonapi.Field::new("name", SStr, description="the user's display name"),
    @moonapi.Field::new("email", SStr),
    @moonapi.Field::new("age", SInt, required=false),
  ])
}

///|
/// The response of both user routes. It nests `Address` and carries a list, so
/// its schema exercises objects, `$ref`s and arrays together.
#derive(ToJson)
struct User {
  id : Int32
  name : String
  email : String
  age : Int32?
  address : Address
  tags : Array[String]
}

///|
fn User::schema() -> @moonapi.Schema {
  @moonapi.Schema::object("User", [
    @moonapi.Field::new("id", SInt),
    @moonapi.Field::new("name", SStr),
    @moonapi.Field::new("email", SStr),
    @moonapi.Field::new("age", SInt, required=false),
    @moonapi.Field::new("address", Address::schema()),
    @moonapi.Field::new("tags", @moonapi.Schema::array(SStr)),
  ])
}

///|
/// `POST /users` takes a typed `NewUser` body and returns a `User`; `GET
/// /users/{id}` takes a typed integer path parameter and returns a `User`. Both
/// validate off their descriptor, and both show typed bodies with `$ref`s in the
/// OpenAPI document.
fn users_app() -> @moonapi.App {
  let app = @moonapi.App::new()
  let create = @moonapi.Endpoint::new(request_body=NewUser::schema(), responses=[
    @moonapi.ResponseSpec::new(
      201,
      description="the created user",
      body=User::schema(),
    ),
    @moonapi.ResponseSpec::new(422, description="Validation Error"),
  ])
  app.post(
    "/users",
    ctx => {
      // The body is checked against `NewUser::schema()` and then read into a
      // `NewUser`; a body the schema refuses comes back as a 422.
      match (ctx.body_validated(NewUser::schema()) : Result[NewUser, _]) {
        Err(errs) => @moonapi.unprocessable(errs)
        Ok(body) => {
          let user : User = {
            id: 1,
            name: body.name,
            email: body.email,
            age: body.age,
            address: { city: "Nowhere", zip: "00000", },
            tags: [],
          }
          @moonapi.json(201, Json(user))
        }
      }
    },
    summary="create a user",
    endpoint=create,
  )
  let get_one = @moonapi.Endpoint::new(
    params=[@moonapi.Param::new("id", InPath, schema=SInt)],
    responses=[
      @moonapi.ResponseSpec::new(200, body=User::schema()),
      @moonapi.ResponseSpec::new(404, description="Not Found"),
    ],
  )
  app.get(
    "/users/{id}",
    ctx => {
      let errs = get_one.validate(ctx)
      if errs.length() > 0 {
        @moonapi.unprocessable(errs)
      } else {
        // The descriptor has already checked this is an integer.
        let id = @string.parse_int(ctx.param("id").unwrap()) catch { _ => 0 }
        let user : User = {
          id,
          name: "demo",
          email: "demo@example.com",
          age: None,
          address: { city: "Springfield", zip: "12345", },
          tags: ["a", "b"],
        }
        @moonapi.json(200, Json(user))
      }
    },
    summary="fetch a user",
    endpoint=get_one,
  )
  app
}