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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +12 -0
- data/CONTRIBUTING.md +26 -0
- data/LICENSE.txt +22 -0
- data/README.md +227 -0
- data/lib/liminal/adapters/dry_schema_v1.rb +115 -0
- data/lib/liminal/compiler.rb +373 -0
- data/lib/liminal/components.rb +90 -0
- data/lib/liminal/dialects/openapi_30/metadata_validator.rb +123 -0
- data/lib/liminal/dialects/openapi_30.rb +363 -0
- data/lib/liminal/errors.rb +59 -0
- data/lib/liminal/internal/normalizer.rb +247 -0
- data/lib/liminal/internal/schema_merge.rb +95 -0
- data/lib/liminal/path.rb +35 -0
- data/lib/liminal/predicate_registry.rb +235 -0
- data/lib/liminal/schema.rb +95 -0
- data/lib/liminal/version.rb +5 -0
- data/lib/liminal.rb +39 -0
- metadata +175 -0
|
@@ -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
|
data/lib/liminal/path.rb
ADDED
|
@@ -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
|
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: []
|