Class: Dry::Types::JSONSchema

Inherits:
Object
  • Object
show all
Defined in:
lib/dry/types/extensions/json_schema.rb

Overview

The ‘JSONSchema` class is responsible for converting dry-types type definitions into JSON Schema definitions. This class enables the transformation of complex type constraints into a standardized JSON Schema format, facilitating interoperability with systems that utilize JSON Schema for validation.

Constant Summary collapse

UnknownPredicateError =

Error raised when an unknown predicate is encountered during schema generation.

Class.new(StandardError)
EMPTY_HASH =

Constant definitions for various lambdas and mappings used throughout the JSON schema conversion process.

{}.freeze
IDENTITY =
->(v, _) { v }.freeze
INSPECT =
->(v, _) { v.inspect }.freeze
TO_INTEGER =
->(v, _) { v.to_i }.freeze
TO_ARRAY =
->(v, _) { Array(v) }.freeze
TO_TYPE =
->(v, _) { CLASS_TO_TYPE.fetch(v.to_s.to_sym) }.freeze
ANNOTATIONS =

Metadata annotations and allowed types overrides for schema generation.

%i[title description].freeze
ALLOWED_TYPES_META_OVERRIDES =
ANNOTATIONS.dup.concat([:format]).freeze
ARRAY_PREDICATE_OVERRIDE =

Mapping for array predicate overrides.

{
  min_size?: :min_items?,
  max_size?: :max_items?
}.freeze
CLASS_TO_TYPE =

Mapping of Ruby classes to their corresponding JSON Schema types.

{
  String:     :string,
  Integer:    :integer,
  TrueClass:  :boolean,
  FalseClass: :boolean,
  NilClass:   :null,
  BigDecimal: :number,
  Float:      :number,
  Hash:       :object,
  Array:      :array,
  Date:       :string,
  DateTime:   :string,
  Time:       :string
}.freeze
EXTRA_PROPS_FOR_TYPE =

Additional properties for specific types, such as formatting options.

{
  Date:     { format: :date },
  Time:     { format: :time },
  DateTime: { format: :"date-time" }
}.freeze
PREDICATE_TO_TYPE =

Mapping of predicate methods to their corresponding JSON Schema expressions.

{
  type?:        { type: TO_TYPE },
  min_size?:    { minLength: TO_INTEGER },
  min_items?:   { minItems: TO_INTEGER },
  max_size?:    { maxLength: TO_INTEGER },
  max_items?:   { maxItems: TO_INTEGER },
  min?:         { maxLength: TO_INTEGER },
  gt?:          { exclusiveMinimum: IDENTITY },
  gteq?:        { minimum: IDENTITY },
  lt?:          { exclusiveMaximum: IDENTITY },
  lteq?:        { maximum: IDENTITY },
  format?:      { format: INSPECT },
  included_in?: { enum: TO_ARRAY }
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root: false, loose: false) ⇒ JSONSchema

Initializes a new instance of the JSONSchema class.

Parameters:

  • root (Boolean) (defaults to: false)

    whether this schema is the root schema.

  • loose (Boolean) (defaults to: false)

    whether to ignore unknown predicates.



85
86
87
88
89
90
91
# File 'lib/dry/types/extensions/json_schema.rb', line 85

def initialize(root: false, loose: false)
  @keys = EMPTY_HASH.dup
  @required = Set.new

  @root = root
  @loose = loose
end

Instance Attribute Details

#requiredSet (readonly)

Returns the set of required keys for the JSON Schema.

Returns:

  • (Set)

    the set of required keys for the JSON Schema.



79
80
81
# File 'lib/dry/types/extensions/json_schema.rb', line 79

def required
  @required
end

Instance Method Details

#call(ast) ⇒ void

This method returns an undefined value.

Processes the abstract syntax tree (AST) and generates the JSON Schema.

Parameters:

  • ast (Array)

    the abstract syntax tree representing type definitions.



107
108
109
# File 'lib/dry/types/extensions/json_schema.rb', line 107

def call(ast)
  visit(ast)
end

#loose?Boolean

Checks if unknown predicates are ignored.

Returns:

  • (Boolean)

    true if ignoring unknown predicates; otherwise, false.



101
# File 'lib/dry/types/extensions/json_schema.rb', line 101

def loose? = @loose

#root?Boolean

Checks if the schema is the root schema.

Returns:

  • (Boolean)

    true if this is the root schema; otherwise, false.



96
# File 'lib/dry/types/extensions/json_schema.rb', line 96

def root?  = @root

#to_hashHash

Converts the internal schema representation into a hash.

Returns:

  • (Hash)

    the JSON Schema as a hash.



114
115
116
117
118
# File 'lib/dry/types/extensions/json_schema.rb', line 114

def to_hash
  result = @keys.to_hash
  result[:$schema] = "http://json-schema.org/draft-06/schema#" if root?
  result
end

#visit(node, opts = EMPTY_HASH) ⇒ void

This method returns an undefined value.

Visits a node in the abstract syntax tree and processes it according to its type.

Parameters:

  • node (Array)

    the node to process.

  • opts (Hash) (defaults to: EMPTY_HASH)

    optional parameters for node processing.



125
126
127
128
# File 'lib/dry/types/extensions/json_schema.rb', line 125

def visit(node, opts = EMPTY_HASH)
  name, rest = node
  public_send(:"visit_#{name}", rest, opts)
end

#visit_and(node, opts = EMPTY_HASH) ⇒ Object



208
209
210
211
212
213
214
# File 'lib/dry/types/extensions/json_schema.rb', line 208

def visit_and(node, opts = EMPTY_HASH)
  left, right = node
  (_, (_, ((_, left_type),))) = left

  visit(left, opts)
  visit(right, { left_type: left_type }.merge(opts))
end

#visit_array(node, opts = EMPTY_HASH) ⇒ Object



230
231
232
233
234
235
236
# File 'lib/dry/types/extensions/json_schema.rb', line 230

def visit_array(node, opts = EMPTY_HASH)
  type, meta = node

  visit(type, { array: true }.merge(opts))

  @keys[opts[:key]].merge!(meta.slice(*ANNOTATIONS)) if meta.any?
end

#visit_constrained(node, opts = EMPTY_HASH) ⇒ Object



130
131
132
# File 'lib/dry/types/extensions/json_schema.rb', line 130

def visit_constrained(node, opts = EMPTY_HASH)
  node.each { |it| visit(it, opts) }
end

#visit_constructor(node, opts = EMPTY_HASH) ⇒ Object



134
135
136
137
138
# File 'lib/dry/types/extensions/json_schema.rb', line 134

def visit_constructor(node, opts = EMPTY_HASH)
  type, _ = node

  visit(type, opts)
end

#visit_enum(node, opts = EMPTY_HASH) ⇒ Object



253
254
255
256
# File 'lib/dry/types/extensions/json_schema.rb', line 253

def visit_enum(node, opts = EMPTY_HASH)
  enum, _ = node
  visit(enum, opts)
end

#visit_hash(node, opts = EMPTY_HASH) ⇒ Object



216
217
218
219
220
# File 'lib/dry/types/extensions/json_schema.rb', line 216

def visit_hash(node, opts = EMPTY_HASH)
  _part, _meta = node

  @keys[opts[:key]] = { type: :object }
end

#visit_intersection(node, opts = EMPTY_HASH) ⇒ Object



183
184
185
186
187
188
189
# File 'lib/dry/types/extensions/json_schema.rb', line 183

def visit_intersection(node, opts = EMPTY_HASH)
  *types, _ = node

  result = types.map { |type| compile_type(type) }

  @keys[opts[:key]] = deep_merge_items(result)
end

#visit_key(node, opts = EMPTY_HASH) ⇒ Object



258
259
260
261
262
263
264
# File 'lib/dry/types/extensions/json_schema.rb', line 258

def visit_key(node, opts = EMPTY_HASH)
  name, required, rest = node

  @required << name if required

  visit(rest, { key: name }.merge(opts))
end

#visit_nominal(node, opts = EMPTY_HASH) ⇒ Object



140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/dry/types/extensions/json_schema.rb', line 140

def visit_nominal(node, opts = EMPTY_HASH)
  type, meta = node

  if opts.key?(:key)
    visit_nominal_with_key(node, opts)
  else
    if opts.key?(:array)
      @keys.merge!(items: { type: CLASS_TO_TYPE[type.to_s.to_sym] })
    else
      @keys.merge!(type: CLASS_TO_TYPE[type.to_s.to_sym])
    end
    @keys.merge!(meta.slice(*ALLOWED_TYPES_META_OVERRIDES)) if meta.any?
  end
end

#visit_predicate(node, opts = EMPTY_HASH) ⇒ Object



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/dry/types/extensions/json_schema.rb', line 155

def visit_predicate(node, opts = EMPTY_HASH)
  head, ((_, type),) = node
  ctx = opts[:key]

  head = ARRAY_PREDICATE_OVERRIDE.fetch(head) if opts[:left_type] == ::Array

  definition = PREDICATE_TO_TYPE.fetch(head) do
    raise UnknownPredicateError, head unless loose?

    EMPTY_HASH
  end.dup

  definition.transform_values! { |v| v.call(type, ctx) }

  return unless definition.any?

  if (extra = EXTRA_PROPS_FOR_TYPE[type.to_s.to_sym])
    definition = definition.merge(extra)
  end

  if ctx.nil?
    @keys.merge!(definition)
  else
    @keys[ctx] ||= {}
    @keys[ctx].merge!(definition)
  end
end

#visit_schema(node, opts = EMPTY_HASH) ⇒ Object



238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/dry/types/extensions/json_schema.rb', line 238

def visit_schema(node, opts = EMPTY_HASH)
  keys, _, meta = node

  target = self.class.new

  keys.each { |fragment| target.visit(fragment, opts) }

  definition = { type: :object, properties: target.to_hash }

  definition[:required] = target.required.to_a if target.required.any?
  definition.merge!(meta.slice(*ANNOTATIONS))  if meta.any?

  @keys.merge!(definition)
end

#visit_struct(node, opts = EMPTY_HASH) ⇒ Object



222
223
224
225
226
227
228
# File 'lib/dry/types/extensions/json_schema.rb', line 222

def visit_struct(node, opts = EMPTY_HASH)
  _, schema = node

  return visit(schema, opts) unless opts[:key]

  @keys[opts[:key]] = compile_type(schema)
end

#visit_sum(node, opts = EMPTY_HASH) ⇒ Object



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/dry/types/extensions/json_schema.rb', line 191

def visit_sum(node, opts = EMPTY_HASH)
  *types, _ = node

  result = types
    .map { |type| compile_value(type, { sum: true }.merge(opts)) }
    .uniq

  return @keys[opts[:key]] = result.first if result.count == 1

  return @keys[opts[:key]] = { anyOf: result } unless opts[:array]

  @keys[opts[:key]] = {
    type: :array,
    items: { anyOf: result }
  }
end