// A worked example of the descriptor tree: user structs that describe their own
// schema, an `impl ToSchema`, and a demo app whose request/response bodies show
// up fully-typed in `openapi.json`. This is the mctl-friendly shape — mctl would
// generate exactly this from a spec: `derive(ToJson)` for serialisation plus a
// plain `T::schema()` associated function (no instance needed, mirroring FastAPI
// referencing the model class) and its one-line `ToSchema` bridge.

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

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

///|
/// `Address`'s schema, so a route declaring it as a body or response documents itself.
pub impl ToSchema for Address with fn to_schema(_self) {
  Address::schema()
}

///|
/// The demo request model — the body of `POST /users`. `age` is optional.
pub(all) struct NewUser {
  name : String
  email : String
  age : Int
} derive(ToJson, Eq)

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

///|
/// `NewUser`'s schema, so a route declaring it as a body or response documents itself.
pub impl ToSchema for NewUser with fn to_schema(_self) {
  NewUser::schema()
}

///|
/// The demo response model — returned by both user routes. Nests `Address` and
/// carries an array of tags, so its emitted schema exercises objects, `$ref`s,
/// and arrays together.
pub(all) struct User {
  id : Int
  name : String
  address : Address
  tags : Array[String]
} derive(ToJson, Eq)

///|
/// The `User` descriptor.
pub fn User::schema() -> Schema {
  Schema::object("User", [
    Field::new("id", SInt),
    Field::new("name", SStr),
    Field::new("address", Address::schema()),
    Field::new("tags", Schema::array(SStr)),
  ])
}

///|
/// `User`'s schema, so a route declaring it as a body or response documents itself.
pub impl ToSchema for User with fn to_schema(_self) {
  User::schema()
}

///|
/// A demo application exercising the descriptor tree end to end: `POST /users`
/// takes a typed `NewUser` body and returns a `User`; `GET /users/:id` takes a
/// typed integer path param and returns a `User`. Both validate off their
/// descriptor and both surface fully-typed bodies (with `components/schemas`
/// `$ref`s) in `openapi.json`.
pub fn demo_app() -> App {
  let app = App::new()
  let create = Endpoint::new(request_body=Some(NewUser::schema()), responses=[
    ResponseSpec::new(
      201,
      description="the created user",
      body=Some(User::schema()),
    ),
    ResponseSpec::new(422, description="Validation Error"),
  ])
  app.post(
    "/users",
    ctx => {
      let errs = create.validate(ctx)
      if errs.length() > 0 {
        unprocessable(errs)
      } else {
        let name = match ctx.json_field("name") {
          Some(String(s)) => s
          _ => ""
        }
        let user : User = {
          id: 1,
          name,
          address: { city: "Nowhere", zip: "00000", },
          tags: [],
        }
        json(201, user.to_json())
      }
    },
    summary="create a user",
    endpoint=Some(create),
  )
  let get_one = Endpoint::new(params=[Param::new("id", InPath, schema=SInt)], responses=[
    ResponseSpec::new(200, body=Some(User::schema())),
    ResponseSpec::new(404, description="Not Found"),
  ])
  app.get(
    "/users/:id",
    ctx => {
      let errs = get_one.validate(ctx)
      if errs.length() > 0 {
        unprocessable(errs)
      } else {
        let id = ctx.param("id").unwrap()
        let user : User = {
          id: parse_int(id).unwrap_or(0),
          name: "demo",
          address: { city: "Springfield", zip: "12345", },
          tags: ["a", "b"],
        }
        json(200, user.to_json())
      }
    },
    summary="fetch a user",
    endpoint=Some(get_one),
  )
  app
}