- Ruby 100%
| .github/workflows | ||
| gemfiles | ||
| lib | ||
| spec | ||
| .gitattributes | ||
| .gitignore | ||
| .rspec | ||
| .rubocop.yml | ||
| CHANGELOG.md | ||
| CONTRIBUTING.md | ||
| Gemfile | ||
| Gemfile.lock | ||
| LICENSE.txt | ||
| liminal.gemspec | ||
| liminal.png | ||
| Rakefile | ||
| README.md | ||
Liminal
Inside the boundaries
Liminal strictly compiles declarative Dry::Schema#to_ast structure and predicates into deterministic OpenAPI 3.0 Schema Objects.
It is useful when a dry-schema definition is the application source of truth and silently dropping a validation constraint would make generated API documentation misleading.
The Ruby API lives under Liminal; it does not use Dry:: and is not an official dry-rb project.
Compatibility
| Concern | Supported |
|---|---|
| Ruby | 3.2 and newer |
| dry-schema | 1.13.4 through 1.16.x (>= 1.13.4, < 1.17) |
| OpenAPI | 3.0.x only |
| Rswag | See liminal-rswag gem |
The dependency matrix runs the same real dry-schema definitions against 1.13, 1.14, 1.15, and 1.16. OpenAPI 3.1 syntax,
including type arrays and numeric exclusiveMinimum values, is rejected.
Installation
Add the gem to your bundle:
gem "liminal", "~> 0.1.0"
Then run bundle install and require liminal.
Compilation
require "dry/schema"
require "liminal"
schema = Dry::Schema.Params do
required(:name).filled(:string)
optional(:score).filled(:float, gt?: 0)
end
openapi_schema = Liminal.compile(
schema,
dialect: :openapi_3_0,
source_name: "CreateEventSchema"
)
The result is a plain recursively string-keyed Hash. Required fields become required; optional fields do not (duh!).
Arrays always include items, empty required arrays are omitted, and stable AST order produces stable output.
Liminal depends on dry-schema, not dry-validation.
Applications using contracts can expose a convenience method:
class Contracts::Base < Dry::Validation::Contract
def self.openapi_schema(**options)
Liminal.compile(schema, source_name: name, **options)
end
end
Dry::Schema::Params may coerce input strings before validation, while OpenAPI describes their post-coercion JSON representation.
Use Dry::Schema::JSON when differential tests should compare non-coercing behavior directly.
Exclusions
Array paths are canonical and preserve property names containing dots:
Liminal.compile(
schema,
exclude: [[:message_id], %i[data internal_metadata]]
)
Exclusions happen before the field rule is compiled, remove the field from its parent's required list, and can conceal an intentionally undocumented unsupported predicate. Unknown paths raise Liminal::UnknownExclusionPathError. Dotted strings are accepted only as a convenience.
Overrides
An enrichment override deep-merges documentation or representable structural details into a successfully generated field:
overrides = {
%i[data attributes event_at] => {
"format" => "date",
"description" => "Calendar date"
}
}
required arrays are unioned during enrichment. An authoritative override may replace a field whose node or predicate cannot be compiled, but it must contain one of type, $ref, oneOf, anyOf, or allOf:
overrides = {
[:opaque_id] => {"type" => "string", "format" => "uuid"}
}
A description alone never hides an unsupported construct. Unknown paths raise Liminal::UnknownOverridePathError.
OpenAPI 3.1-only keywords are rejected. OpenAPI 3.0 forbids $ref siblings, so use allOf when a reference also needs a
description:
{ "allOf" => [{ "$ref" => "#/components/schemas/User" }], "description" => "User" }
Predicate extensions
The default registry is immutable. with returns a copy:
predicates = Liminal::PredicateRegistry.default.with(:uuid_v4?) do |_arguments, context|
raise "unexpected dialect" unless context.dialect.name == :openapi_3_0
{"type" => "string", "format" => "uuid"}
end
Liminal.compile(schema, predicates: predicates)
Handlers receive semantic arguments with dry input placeholders removed and a context containing the canonical path and
dialect. They must return a Hash. Duplicate registration requires replace: true; incompatible keywords and types are
rejected.
Built-in predicates cover arrays, booleans, dates and times, numbers, hashes, integers, strings, type? for the documented
Ruby primitive classes, equality and inclusion enums, safe regular expressions, filled and size constraints, numeric
boundaries, and integer parity. and, or, nullable implications, and each are compiled without exposing raw AST nodes.
Strict failures and custom rules
Unsupported AST nodes and predicates raise contextual Liminal::UnsupportedNodeError or
Liminal::UnsupportedPredicateError, including the source, canonical field path, and failing operation. Compilation never
has a permissive mode.
A dry-validation rule such as this is arbitrary Ruby and is not part of the declarative schema AST:
rule("data.attributes.event_at") do
ValidateIso8601Date.call(date: value)
end
Liminal does not execute, introspect, or pretend to translate such rules. Represent structural documentation explicitly with an override. Database checks and cross-field business rules belong in endpoint prose rather than a Schema Object.
Components
Register a generated component in any mutable OpenAPI document:
reference = Liminal::Components.register(
document: openapi_document,
name: :calendar_event,
schema: openapi_schema
)
# => {"$ref"=>"#/components/schemas/calendar_event"}
Missing maps are created. String and symbol document keys and component names are treated logically alike. Re-registering
the same normalized schema is idempotent; a different schema raises Liminal::ComponentCollisionError.
Rswag boundary
The core gem contains no Rails, Active Support, RSpec, or Rswag runtime dependency and performs no global monkey-patching. Until a separately versioned Rswag companion is requested, an application-side opt-in helper can keep the integration at the metadata boundary:
def request_body_from_schema(schema:, component:, parameter_name:, source_name: nil, **options)
generated = Liminal.compile(schema, source_name: source_name, **options)
reference = Liminal::Components.register(
document: selected_openapi_document,
name: component,
schema: generated
)
parameter name: parameter_name, in: :body, required: true, schema: reference
end
Application action-to-contract metadata remains outside Liminal. Request examples remain Rswag metadata and are not embedded into structural schemas.
Development
Run bundle exec rake for randomized RSpec and RuboCop checks, COVERAGE=true bundle exec rspec for line and branch
coverage, and bundle exec rake build to build the gem locally. The development suite validates a generated minimal
document with openapi3_parser and compares representative non-coercing payload behavior with json_schemer.
The project is versioned from 0.1.0 while its extension API is being proven. No release or publication is performed by
the implementation workflow.
