Documentation Generation

A Schema instance has a #structure method that return Paradocs::Extensions::Structure instance that allows instrospecting schema meta data.

It's supposed to have the following schema:

schema = Paradocs::Schema.new do
  field(:data).type(:object).present.schema do
    field(:id).type(:integer).present.policy(:policy_with_error)
    field(:name).type(:string).meta(label: "very important staff")
    field(:role).type(:string).declared.options(["admin", "user"]).default("user").mutates_schema! do |*|
      :test_subschema
    end
    field(:extra).type(:array).required.schema do
      field(:extra).declared.default(false).policy(:policy_with_silent_error)
    end

    mutation_by!(:name) { :subschema }

    subschema(:subschema) do
      field(:test_field).present
    end
    subschema(:test_subschema) do
      field(:test1).present
    end
  end
end

Structure#nested

This method returns schema structure in a nested way including subschemes.

schema.structure.nested.to_json # =>
{
  "_errors": ["ArgumentError"],
  "_subschemes": {},
  "data": {
    "type": "object",
    "required": true,
    "present": true,
    "json_path": "$.data",
    "nested_name": "data",
    "structure": {
      "_subschemes": {
        "subschema": {
          "_errors": [],
          "_subschemes": {},
          "test_field": {
            "required": true,
            "present": true,
            "json_path": "$.data.test_field",
            "nested_name": "data.test_field"
          }
        },
        "test_subschema": {
          "_errors": [],
          "_subschemes": {},
          "test1": {
            "required": true,
            "present": true,
            "json_path": "$.data.test1",
            "nested_name": "data.test1"
          }
        }
      },
      "id": {
        "type": "integer",
        "required": true,
        "present": true,
        "policy_with_error": {"errors": ["ArgumentError"]},
        "json_path": "$.data.id",
        "nested_name": "data.id"
      },
      "name": {
        "type": "string",
        "label": "very important staff",
        "json_path": "$.data.name",
        "mutates_schema": true,
        "nested_name": "data.name"
      },
      "role": {
        "type": "string",
        "options": ["admin", "user"],
        "default": "user",
        "json_path": "$.data.role",
        "mutates_schema": true,
        "nested_name": "data.role"
      },
      "extra": {
        "type": "array",
        "required": true,
        "json_path": "$.data.extra[]",
        "nested_name": "data.extra",
        "structure": {
          "_subschemes": {},
          "extra": {
            "default": false,
            "policy_with_silent_error": {"errors": []},
            "json_path": "$.data.extra[].extra",
            "nested_name": "data.extra.extra"
          }
        }
      }
    }
  }
}

Structure#flatten

This method returns schema structure in a flatten (without deep nesting) way including subschemes.

schema.structure.flatten.to_json # =>
{
  "_errors": ["ArgumentError"],
  "_subschemes": {
    "subschema": {
      "_errors": [],
      "_subschemes": {},
      "data.test_field": {
        "required": true,
        "present": true,
        "json_path": "$.data.test_field"
      }
    },
    "test_subschema": {
      "_errors": [],
      "_subschemes": {},
      "data.test1": {
        "required": true,
        "present": true,
        "json_path": "$.data.test1"
      }
    }
  },
  "data": {
    "type": "object",
    "required": true,
    "present": true,
    "json_path": "$.data"
  },
  "data.id": {
    "type": "integer",
    "required": true,
    "present": true,
    "policy_with_error": {"errors": ["ArgumentError"]},
    "json_path": "$.data.id"
  },
  "data.name": {
    "type": "string",
    "label": "very important staff",
    "json_path": "$.data.name",
    "mutates_schema": true
  },
  "data.role": {
    "type": "string",
    "options": ["admin", "user"],
    "default": "user",
    "json_path": "$.data.role",
    "mutates_schema": true
  },
  "data.extra": {
    "type": "array",
    "required": true,
    "json_path": "$.data.extra[]"
  },
  "data.extra.extra": {
    "default": false,
    "policy_with_silent_error": {"errors": []},
    "json_path": "$.data.extra[].extra"
  }
}

Structure#all_nested

This method returns all available combinations of schema (built on subschemas) saving the nesting.

Will return a hash with 2 structures named by the names of declared subschemas:

all_nested = schema.structure.all_nested
all_nested.keys # => [:subschema, :test_subschema]
all_nested[:subschema] # =>
{
  _errors: [],
  "data" => {
    type:      :object,
    required:  true,
    present:   true,
    json_path: "$.data",
    nested_name: "data",
    structure: {
      "role"       => {type: :string, options: ["admin", "user"], default: "user", json_path: "$.data.role", mutates_schema: true, nested_name: "data.role"},
      "test_field" => {required: true, present: true, json_path: "$.data.test_field", nested_name: "data.test_field"},
      "id"         => {type: :integer, required: true, present: true, policy_with_error: {errors: [ArgumentError]}, json_path: "$.data.id", nested_name: "data.id"},
      "name"       => {type: :string, label: "very important staff", json_path: "$.data.name", mutates_schema: true, nested_name: "data.name"},
      "extra"      => {
        type: :array, required: true, json_path: "$.data.extra[]", nested_name: "data.extra",
        structure: {"extra" => {default: false, policy_with_silent_error: {errors: []}, json_path: "$.data.extra[].extra", nested_name: "data.extra.extra"}}
      }
    }
  }
}
all_nested[:test_subschema] # =>
{
  _errors:     [],
  "data" => {
    type:      :object,
    required:  true,
    present:   true,
    json_path: "$.data",
    nested_name: "data",
    structure: {
      "role"  => {type: :string, options: ["admin", "user"], default: "user", json_path: "$.data.role", mutates_schema: true, nested_name: "data.role"},
      "test1" => {required: true, present: true, json_path: "$.data.test1", nested_name: "data.test1"},
      "id"    => {type: :integer, required: true, present: true, policy_with_error: {errors: [ArgumentError]}, json_path: "$.data.id", nested_name: "data.id"},
      "name"  => {type: :string, label: "very important staff", json_path: "$.data.name", mutates_schema: true, nested_name: "data.name"},
      "extra" => {
        type: :array, required: true, json_path: "$.data.extra[]", nested_name: "data.extra",
        structure: {"extra" => {default: false, policy_with_silent_error: {errors: []}, json_path: "$.data.extra[].extra", nested_name: "data.extra.extra"}}
      }
    }
  }
}

Structure#all_flatten

This method returns all available combinations of schema (built on subschema) without nesting (the same way as Structure#flatten method does)

Schema is the same as described in Structure#all_nested

schema.structure.all_flatten # =>
{
  subschema: {
    _errors: [],
    "data"             => {type: :object, required: true, present: true, json_path: "$.data", nested_name: "data"},
    "data.id"          => {type: :integer, required: true, present: true, policy_with_error: {errors: [ArgumentError]}, json_path: "$.data.id", nested_name: "data.id"},
    "data.name"        => {type: :string, label: "very important staff", json_path: "$.data.name", mutates_schema: true, nested_name: "data.name"},
    "data.role"        => {type: :string, options: ["admin", "user"], default: "user", json_path: "$.data.role", mutates_schema: true, nested_name: "data.role"},
    "data.extra"       => {type: :array, required: true, json_path: "$.data.extra[]", nested_name: "data.extra"},
    "data.extra.extra" => {default: false, policy_with_silent_error: {errors: []}, json_path: "$.data.extra[].extra", nested_name: "data.extra.extra"},
    "data.test_field"  => {required: true, present: true, json_path: "$.data.test_field", nested_name: "data.test_field"}
  },
  test_subschema: {
    _errors: [],
    "data"             => {type: :object, required: true, present: true, json_path: "$.data", nested_name: "data"},
    "data.id"          => {type: :integer, required: true, present: true, policy_with_error: {errors: [ArgumentError]}, json_path: "$.data.id", nested_name: "data.id"},
    "data.name"        => {type: :string, label: "very important staff", json_path: "$.data.name", mutates_schema: true, nested_name: "data.name"},
    "data.role"        => {type: :string, options: ["admin", "user"], default: "user", json_path: "$.data.role", mutates_schema: true, nested_name: "data.role"},
    "data.extra"       => {type: :array, required: true, json_path: "$.data.extra[]", nested_name: "data.extra"},
    "data.extra.extra" => {default: false, policy_with_silent_error: {errors: []}, json_path: "$.data.extra[].extra", nested_name: "data.extra.extra"},
    "data.test1"       => {required: true, present: true, json_path: "$.data.test1", nested_name: "data.test1"}
  }
}

Passing a block

Given block to the methods above (#flatten, #nested, #all_flatten, #all_nested) will be executed for each field, passing you as arguments field.key and field.meta. Mutating the second argument field.meta will reflect onto returned meta.

Schema#walk

The #walk method can recursively walk a schema definition and extract meta data or field attributes.

schema_documentation = create_user_schema.walk do |field|
  {type: field.meta_data[:type], label: field.meta_data[:label]}
end.output

# Returns

{
  name: {type: :string, label: "User's full name"},
  age: {type: :integer, label: "User's age"},
  status: {type: :string, label: nil},
  friends: [
    {
      name: {type: :string, label: "Friend full name"},
      email: {type: nil, label: "Friend email"}
    }
  ]
}

When passed a symbol, it will collect that key from field meta data.

schema_labels = create_user_schema.walk(:label).output

# returns

{
  name: "User's full name",
  age: "User's age",
  status: nil,
  friends: [
    {name: "Friend full name", email: "Friend email"}
  ]
}

Potential uses for this are generating documentation (HTML, or JSON Schema, Swagger, or maybe even mock API endpoints with example data.