liminal 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liminal
4
+ module Internal
5
+ # Deterministic schema-aware merge operations.
6
+ module SchemaMerge
7
+ module_function
8
+
9
+ # Combine two conjunctive schema fragments without weakening either one.
10
+ def combine(left, right)
11
+ left = deep_copy(left)
12
+ right = deep_copy(right)
13
+ return right if left.empty?
14
+ return left if right.empty?
15
+ return left if left == right
16
+
17
+ merged = left.dup
18
+
19
+ right.each do |key, right_value|
20
+ unless merged.key?(key)
21
+ merged[key] = right_value
22
+ next
23
+ end
24
+
25
+ left_value = merged[key]
26
+ merged_value = combine_value(key, left_value, right_value)
27
+ return all_of(left, right) if merged_value.equal?(CONFLICT)
28
+
29
+ merged[key] = merged_value
30
+ end
31
+
32
+ merged
33
+ end
34
+
35
+ # Apply an enrichment override. Override values win except that required
36
+ # arrays are unioned and nested hashes are recursively merged.
37
+ def override(generated, supplied)
38
+ result = generated.each_with_object({}) do |(key, value), copy|
39
+ copy[key] = deep_copy(value)
40
+ end
41
+
42
+ supplied.each do |key, value|
43
+ result[key] = if key == "required" && result[key].is_a?(Array) && value.is_a?(Array)
44
+ ordered_union(result[key], value)
45
+ elsif result[key].is_a?(Hash) && value.is_a?(Hash)
46
+ override(result[key], value)
47
+ else
48
+ deep_copy(value)
49
+ end
50
+ end
51
+
52
+ result
53
+ end
54
+
55
+ def deep_copy(value)
56
+ case value
57
+ when Hash
58
+ value.each_with_object({}) { |(key, item), copy| copy[key] = deep_copy(item) }
59
+ when Array
60
+ value.map { |item| deep_copy(item) }
61
+ else
62
+ value
63
+ end
64
+ end
65
+
66
+ def ordered_union(left, right)
67
+ (left + right).each_with_object([]) do |value, values|
68
+ values << value unless values.include?(value)
69
+ end
70
+ end
71
+
72
+ def combine_value(key, left, right)
73
+ return left if left == right
74
+ return combine(left, right) if left.is_a?(Hash) && right.is_a?(Hash)
75
+ return ordered_union(left, right) if key == "required" && left.is_a?(Array) && right.is_a?(Array)
76
+ return left + right if key == Normalizer::CONSTRAINTS_KEY && left.is_a?(Array) && right.is_a?(Array)
77
+
78
+ CONFLICT
79
+ end
80
+ private_class_method :combine_value
81
+
82
+ def all_of(left, right)
83
+ alternatives = [left, right].flat_map do |fragment|
84
+ fragment.keys == ["allOf"] ? fragment.fetch("allOf") : [fragment]
85
+ end
86
+
87
+ {"allOf" => alternatives.each_with_object([]) { |item, list| list << item unless list.include?(item) }}
88
+ end
89
+ private_class_method :all_of
90
+
91
+ CONFLICT = Object.new.freeze
92
+ private_constant :CONFLICT
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liminal
4
+ # Utilities for canonical property paths.
5
+ module Path
6
+ module_function
7
+
8
+ # Convert an array path, dotted convenience string, or single segment to
9
+ # a frozen array of strings.
10
+ def normalize(path)
11
+ segments = case path
12
+ when Array then path
13
+ when String then path.split(".", -1)
14
+ when Symbol then [path]
15
+ else
16
+ raise ArgumentError, "path must be an Array, String, or Symbol"
17
+ end
18
+
19
+ raise ArgumentError, "path cannot be empty" if segments.empty?
20
+
21
+ segments.map do |segment|
22
+ unless segment.is_a?(String) || segment.is_a?(Symbol)
23
+ raise ArgumentError, "path segments must be strings or symbols"
24
+ end
25
+
26
+ segment.to_s.dup.freeze
27
+ end.freeze
28
+ end
29
+
30
+ # Render a path for diagnostics. Arrays remain the canonical API form.
31
+ def display(path)
32
+ path.empty? ? "<root>" : path.join(".")
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+ require "date"
5
+ require "set"
6
+
7
+ module Liminal
8
+ # Context supplied to custom predicate handlers.
9
+ PredicateContext = Struct.new(:path, :dialect, keyword_init: true) do
10
+ def initialize(path:, dialect:)
11
+ immutable_path = path.map { |segment| segment.to_s.dup.freeze }.freeze
12
+ super(path: immutable_path, dialect: dialect)
13
+ freeze
14
+ end
15
+ end
16
+
17
+ # Immutable registry of declarative dry predicate handlers.
18
+ class PredicateRegistry
19
+ Entry = Struct.new(:handler, :internal, keyword_init: true)
20
+ class UnsupportedValue < StandardError; end
21
+
22
+ class << self
23
+ # Return the immutable registry containing all built-in predicates.
24
+ #
25
+ # @return [PredicateRegistry]
26
+ def default
27
+ @default ||= build_default.freeze
28
+ end
29
+
30
+ private
31
+
32
+ def build_default
33
+ registry = new
34
+ primitive_fragments.each do |name, fragment|
35
+ registry = registry.send(:register_internal, name) do |arguments, _context|
36
+ no_arguments!(name, arguments, fragment)
37
+ end
38
+ end
39
+
40
+ registry = add_type_handler(registry)
41
+ registry = add_value_handlers(registry)
42
+ add_size_and_numeric_handlers(registry)
43
+ end
44
+
45
+ def primitive_fragments
46
+ {
47
+ array?: {"type" => "array", "items" => {}},
48
+ bool?: {"type" => "boolean"},
49
+ date?: {"type" => "string", "format" => "date"},
50
+ date_time?: {"type" => "string", "format" => "date-time"},
51
+ decimal?: {"type" => "number"},
52
+ float?: {"type" => "number"},
53
+ number?: {"type" => "number"},
54
+ hash?: {"type" => "object"},
55
+ int?: {"type" => "integer"},
56
+ str?: {"type" => "string"},
57
+ time?: {"type" => "string", "format" => "time"},
58
+ true?: {"type" => "boolean", "enum" => [true]},
59
+ false?: {"type" => "boolean", "enum" => [false]}
60
+ }.freeze
61
+ end
62
+
63
+ def add_type_handler(registry)
64
+ registry.send(:register_internal, :type?) do |arguments, _context|
65
+ type = one_argument!(:type?, arguments)
66
+ fragment = type_fragments[type]
67
+ raise UnsupportedValue, "unsupported Ruby class #{type}" unless fragment
68
+
69
+ fragment
70
+ end
71
+ end
72
+
73
+ def type_fragments
74
+ {
75
+ Array => {"type" => "array", "items" => {}},
76
+ BigDecimal => {"type" => "number"},
77
+ Date => {"type" => "string", "format" => "date"},
78
+ DateTime => {"type" => "string", "format" => "date-time"},
79
+ Float => {"type" => "number"},
80
+ Hash => {"type" => "object"},
81
+ Integer => {"type" => "integer"},
82
+ Numeric => {"type" => "number"},
83
+ String => {"type" => "string"},
84
+ Time => {"type" => "string", "format" => "time"},
85
+ TrueClass => {"type" => "boolean", "enum" => [true]},
86
+ FalseClass => {"type" => "boolean", "enum" => [false]}
87
+ }.freeze
88
+ end
89
+
90
+ def add_value_handlers(registry)
91
+ registry = registry.send(:register_internal, :eql?) do |arguments, _context|
92
+ value = one_argument!(:eql?, arguments)
93
+ ensure_openapi_value!(:eql?, value)
94
+ {"enum" => [value]}
95
+ end
96
+ registry = registry.send(:register_internal, :included_in?) do |arguments, _context|
97
+ values = one_argument!(:included_in?, arguments)
98
+ values = values.to_a if values.is_a?(Set)
99
+ raise UnsupportedValue, "included_in? requires a finite array or set" unless values.is_a?(Array)
100
+ raise UnsupportedValue, "included_in? requires at least one value" if values.empty?
101
+
102
+ values.each { |value| ensure_openapi_value!(:included_in?, value) }
103
+
104
+ {"enum" => values.uniq}
105
+ end
106
+ registry.send(:register_internal, :format?) do |arguments, context|
107
+ regexp = one_argument!(:format?, arguments)
108
+ begin
109
+ {"pattern" => context.dialect.pattern(regexp)}
110
+ rescue Dialects::OpenAPI30::UnsupportedPatternError => e
111
+ raise UnsupportedValue, e.message
112
+ end
113
+ end
114
+ end
115
+
116
+ def add_size_and_numeric_handlers(registry)
117
+ registry = registry.send(:register_internal, :filled?) do |arguments, _context|
118
+ no_arguments!(:filled?, arguments, Internal::Normalizer.constraint(:filled?))
119
+ end
120
+
121
+ %i[min_size? max_size? size?].each do |name|
122
+ registry = registry.send(:register_internal, name) do |arguments, _context|
123
+ Internal::Normalizer.constraint(name, one_argument!(name, arguments))
124
+ end
125
+ end
126
+
127
+ {
128
+ gt?: ->(value) { {"minimum" => value, "exclusiveMinimum" => true} },
129
+ gteq?: ->(value) { {"minimum" => value} },
130
+ lt?: ->(value) { {"maximum" => value, "exclusiveMaximum" => true} },
131
+ lteq?: ->(value) { {"maximum" => value} }
132
+ }.each do |name, builder|
133
+ registry = registry.send(:register_internal, name) do |arguments, _context|
134
+ value = one_argument!(name, arguments)
135
+ raise UnsupportedValue, "#{name} requires a finite numeric argument" unless json_number?(value)
136
+
137
+ builder.call(value)
138
+ end
139
+ end
140
+
141
+ registry = registry.send(:register_internal, :even?) do |arguments, _context|
142
+ no_arguments!(:even?, arguments, {"type" => "integer", "multipleOf" => 2})
143
+ end
144
+ registry.send(:register_internal, :odd?) do |arguments, _context|
145
+ no_arguments!(:odd?, arguments, {"type" => "integer", "not" => {"multipleOf" => 2}})
146
+ end
147
+ end
148
+
149
+ def one_argument!(name, arguments)
150
+ return arguments.first if arguments.length == 1
151
+
152
+ raise UnsupportedValue, "#{name} requires exactly one semantic argument"
153
+ end
154
+
155
+ def no_arguments!(name, arguments, result)
156
+ return result if arguments.empty?
157
+
158
+ raise UnsupportedValue, "#{name} does not accept semantic arguments"
159
+ end
160
+
161
+ def ensure_openapi_value!(name, value)
162
+ return if openapi_value?(value)
163
+
164
+ raise UnsupportedValue, "#{name} value #{value.inspect} is not OpenAPI-compatible"
165
+ end
166
+
167
+ def openapi_value?(value)
168
+ case value
169
+ when nil, true, false, String, Symbol, Integer, Date, Time
170
+ true
171
+ when Numeric
172
+ json_number?(value)
173
+ when Array
174
+ value.all? { |item| openapi_value?(item) }
175
+ when Hash
176
+ keys = value.keys
177
+ keys.all? { |key| key.is_a?(String) || key.is_a?(Symbol) } &&
178
+ keys.map(&:to_s).uniq.length == keys.length && value.values.all? { |item| openapi_value?(item) }
179
+ else
180
+ false
181
+ end
182
+ end
183
+
184
+ def json_number?(value)
185
+ supported = value.is_a?(Integer) || value.is_a?(Float)
186
+ supported && (!value.respond_to?(:finite?) || value.finite?)
187
+ end
188
+ end
189
+
190
+ def initialize(entries = {})
191
+ @entries = entries.freeze
192
+ freeze
193
+ end
194
+
195
+ # Return a new registry containing +name+ without mutating this registry.
196
+ #
197
+ # @param name [String, Symbol] dry predicate name
198
+ # @param replace [Boolean] explicitly replace an existing registration
199
+ # @yieldparam arguments [Array] semantic arguments without input placeholders
200
+ # @yieldparam context [PredicateContext] canonical path and active dialect
201
+ # @yieldreturn [Hash] OpenAPI Schema Object fragment
202
+ # @return [PredicateRegistry]
203
+ def with(name, replace: false, &handler)
204
+ raise ArgumentError, "a predicate handler block is required" unless handler
205
+
206
+ add(name, handler, replace: replace, internal: false)
207
+ end
208
+
209
+ # Look up a predicate entry. Intended for compiler adapters.
210
+ #
211
+ # @param name [String, Symbol]
212
+ # @return [Entry, nil]
213
+ def entry(name)
214
+ @entries[name.to_sym]
215
+ end
216
+
217
+ protected
218
+
219
+ def register_internal(name, &handler)
220
+ add(name, handler, replace: false, internal: true)
221
+ end
222
+
223
+ private
224
+
225
+ def add(name, handler, replace:, internal:)
226
+ predicate = name.to_sym
227
+ if @entries.key?(predicate) && !replace
228
+ raise PredicateAlreadyRegisteredError,
229
+ "Predicate #{predicate} is already registered; pass replace: true to replace it."
230
+ end
231
+
232
+ self.class.new(@entries.merge(predicate => Entry.new(handler: handler, internal: internal).freeze))
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liminal
4
+ # Validation, normalization, and non-mutating composition for OpenAPI Schema
5
+ # Objects.
6
+ module Schema
7
+ module_function
8
+
9
+ def normalize(schema, dialect: :openapi_3_0)
10
+ unless schema.is_a?(Hash)
11
+ raise InvalidSchemaError,
12
+ "Schema Object must be a Hash; received #{schema.class}."
13
+ end
14
+
15
+ validator = build_dialect(dialect)
16
+ normalized = Internal::Normalizer.normalize_keys(schema)
17
+ validator.validate!(normalized)
18
+ validator.order(normalized)
19
+ rescue Dialects::OpenAPI30::InvalidFragmentError => e
20
+ raise InvalidSchemaError, "Invalid OpenAPI 3.0 Schema Object: #{e.message}."
21
+ end
22
+
23
+ def required(schema, *names)
24
+ result = normalize_composition_input(schema)
25
+ normalized_names = normalize_names(names, minimum: 1)
26
+ constraint = {"required" => normalized_names}
27
+
28
+ if result.key?("$ref")
29
+ all_of(result, constraint)
30
+ else
31
+ result["required"] = (Array(result["required"]) | normalized_names)
32
+ normalize(result)
33
+ end
34
+ end
35
+
36
+ def exactly_one(schema, *names)
37
+ result = normalize_composition_input(schema)
38
+ normalized_names = normalize_names(names, minimum: 2)
39
+ constraint = {
40
+ "oneOf" => normalized_names.map { |name| {"required" => [name]} }
41
+ }
42
+ return normalize(result.merge(constraint)) unless result.key?("oneOf") || result.key?("$ref")
43
+
44
+ all_of(result, constraint)
45
+ end
46
+
47
+ def all_of(*schemas)
48
+ if schemas.empty?
49
+ raise InvalidSchemaCompositionError,
50
+ "all_of requires at least one OpenAPI Schema Object."
51
+ end
52
+
53
+ normalize({"allOf" => schemas.map { |schema| normalize_composition_input(schema) }})
54
+ end
55
+
56
+ def build_dialect(name)
57
+ return Dialects::OpenAPI30.new.freeze if name == :openapi_3_0
58
+
59
+ raise UnsupportedDialectError,
60
+ "Unsupported dialect #{name.inspect}; only :openapi_3_0 is available."
61
+ end
62
+ private_class_method :build_dialect
63
+
64
+ def normalize_composition_input(schema)
65
+ return normalize(schema) if schema.is_a?(Hash)
66
+
67
+ raise InvalidSchemaCompositionError,
68
+ "Schema composition requires a Hash; received #{schema.class}."
69
+ end
70
+ private_class_method :normalize_composition_input
71
+
72
+ def normalize_names(names, minimum:)
73
+ normalized = names.map do |name|
74
+ unless name.is_a?(String) || name.is_a?(Symbol)
75
+ raise InvalidSchemaCompositionError,
76
+ "Property names must be non-empty Strings or Symbols; received #{name.class}."
77
+ end
78
+
79
+ value = name.to_s
80
+ if value.strip.empty?
81
+ raise InvalidSchemaCompositionError,
82
+ "Property names must be non-empty Strings or Symbols; received #{name.inspect}."
83
+ end
84
+ value
85
+ end.uniq
86
+
87
+ return normalized if normalized.length >= minimum
88
+
89
+ noun = minimum == 1 ? "name" : "names"
90
+ raise InvalidSchemaCompositionError,
91
+ "Schema composition requires at least #{minimum} distinct property #{noun}."
92
+ end
93
+ private_class_method :normalize_names
94
+ end
95
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liminal
4
+ VERSION = "0.1.0"
5
+ end
data/lib/liminal.rb ADDED
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "liminal/version"
4
+ require "liminal/errors"
5
+ require "liminal/path"
6
+ require "liminal/adapters/dry_schema_v1"
7
+ require "liminal/dialects/openapi_30"
8
+ require "liminal/internal/normalizer"
9
+ require "liminal/internal/schema_merge"
10
+ require "liminal/predicate_registry"
11
+ require "liminal/schema"
12
+ require "liminal/compiler"
13
+ require "liminal/components"
14
+
15
+ # Strict dry-schema to OpenAPI 3.0 compilation.
16
+ module Liminal
17
+ module_function
18
+
19
+ # Compile a Dry::Schema-compatible source into an OpenAPI 3.0 Schema Object.
20
+ #
21
+ # @param schema [#to_ast] declarative dry-schema source
22
+ # @param dialect [Symbol] currently only +:openapi_3_0+
23
+ # @param source_name [String, nil] label included in diagnostics
24
+ # @param exclude [Array<Array, String, Symbol>] property paths to omit
25
+ # @param overrides [Hash] property paths mapped to OpenAPI fragments
26
+ # @param predicates [PredicateRegistry] immutable predicate registry
27
+ # @return [Hash] deterministic Schema Object containing only string keys
28
+ def compile(schema, dialect: :openapi_3_0, source_name: nil, exclude: [], overrides: {},
29
+ predicates: PredicateRegistry.default)
30
+ Compiler.new(
31
+ schema,
32
+ dialect: dialect,
33
+ source_name: source_name,
34
+ exclude: exclude,
35
+ overrides: overrides,
36
+ predicates: predicates
37
+ ).call
38
+ end
39
+ end
metadata ADDED
@@ -0,0 +1,175 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: liminal
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Maurizio De Magnis
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: dry-schema
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 1.13.4
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '1.17'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: 1.13.4
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '1.17'
32
+ - !ruby/object:Gem::Dependency
33
+ name: json_schemer
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '2.4'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '3'
42
+ type: :development
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '2.4'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '3'
52
+ - !ruby/object:Gem::Dependency
53
+ name: openapi3_parser
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - "~>"
57
+ - !ruby/object:Gem::Version
58
+ version: 0.10.1
59
+ type: :development
60
+ prerelease: false
61
+ version_requirements: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - "~>"
64
+ - !ruby/object:Gem::Version
65
+ version: 0.10.1
66
+ - !ruby/object:Gem::Dependency
67
+ name: rake
68
+ requirement: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - "~>"
71
+ - !ruby/object:Gem::Version
72
+ version: '13.0'
73
+ type: :development
74
+ prerelease: false
75
+ version_requirements: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - "~>"
78
+ - !ruby/object:Gem::Version
79
+ version: '13.0'
80
+ - !ruby/object:Gem::Dependency
81
+ name: rspec
82
+ requirement: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - "~>"
85
+ - !ruby/object:Gem::Version
86
+ version: '3.13'
87
+ type: :development
88
+ prerelease: false
89
+ version_requirements: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - "~>"
92
+ - !ruby/object:Gem::Version
93
+ version: '3.13'
94
+ - !ruby/object:Gem::Dependency
95
+ name: rubocop
96
+ requirement: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: '1.64'
101
+ - - "<"
102
+ - !ruby/object:Gem::Version
103
+ version: '2'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '1.64'
111
+ - - "<"
112
+ - !ruby/object:Gem::Version
113
+ version: '2'
114
+ - !ruby/object:Gem::Dependency
115
+ name: simplecov
116
+ requirement: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - "~>"
119
+ - !ruby/object:Gem::Version
120
+ version: '0.22'
121
+ type: :development
122
+ prerelease: false
123
+ version_requirements: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - "~>"
126
+ - !ruby/object:Gem::Version
127
+ version: '0.22'
128
+ description: |
129
+ Liminal converts the documented Dry::Schema AST representation into
130
+ deterministic, string-keyed OpenAPI 3.0 Schema Objects.
131
+ email: root@olisti.co
132
+ executables: []
133
+ extensions: []
134
+ extra_rdoc_files: []
135
+ files:
136
+ - CHANGELOG.md
137
+ - CONTRIBUTING.md
138
+ - LICENSE.txt
139
+ - README.md
140
+ - lib/liminal.rb
141
+ - lib/liminal/adapters/dry_schema_v1.rb
142
+ - lib/liminal/compiler.rb
143
+ - lib/liminal/components.rb
144
+ - lib/liminal/dialects/openapi_30.rb
145
+ - lib/liminal/dialects/openapi_30/metadata_validator.rb
146
+ - lib/liminal/errors.rb
147
+ - lib/liminal/internal/normalizer.rb
148
+ - lib/liminal/internal/schema_merge.rb
149
+ - lib/liminal/path.rb
150
+ - lib/liminal/predicate_registry.rb
151
+ - lib/liminal/schema.rb
152
+ - lib/liminal/version.rb
153
+ homepage: https://source.olisti.co/olisti.co/liminal
154
+ licenses:
155
+ - MIT
156
+ metadata:
157
+ rubygems_mfa_required: 'true'
158
+ rdoc_options: []
159
+ require_paths:
160
+ - lib
161
+ required_ruby_version: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '3.2'
166
+ required_rubygems_version: !ruby/object:Gem::Requirement
167
+ requirements:
168
+ - - ">="
169
+ - !ruby/object:Gem::Version
170
+ version: '0'
171
+ requirements: []
172
+ rubygems_version: 4.0.15
173
+ specification_version: 4
174
+ summary: Strictly compile dry-schema ASTs into OpenAPI 3.0 schemas
175
+ test_files: []