• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

notEthan / jsi / 17510396325

06 Sep 2025 05:12AM UTC coverage: 99.026% (-0.009%) from 99.035%
17510396325

push

github

notEthan
Merge branches 'base.conf.child_defaults', 'msn.modified_copy', 'msn.conf.is_metaschema' and 'misc822' into HEAD

65 of 66 new or added lines in 9 files covered. (98.48%)

10 existing lines in 2 files now uncovered.

7419 of 7492 relevant lines covered (99.03%)

138415.08 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.33
/lib/jsi/schema_classes.rb
1
# frozen_string_literal: true
2

3
module JSI
56✔
4
  # A Module associated with a JSI Schema (its {Schema#jsi_schema_module #jsi_schema_module}).
5
  #
6
  # This module may be opened by the application to define methods for instances described by its schema.
7
  #
8
  # The schema module can also be used in some of the same ways as its schema:
9
  # JSI instances of the schema can be instantiated using {#new_jsi}, or instances
10
  # can be validated with {#instance_valid?} or {#instance_validate}.
11
  # Often the schema module is the more convenient object to work with with than the JSI Schema.
12
  #
13
  # Naming the schema module (assigning it to a constant) can be useful in a few ways.
14
  #
15
  # - When inspected, instances of a schema with a named schema module will show that name.
16
  # - Naming the module allows it to be opened with Ruby's `module` syntax. Any schema module
17
  #   can be opened with [Module#module_exec](https://ruby-doc.org/core/Module.html#method-i-module_exec)
18
  #   (or from the Schema with {Schema#jsi_schema_module_exec jsi_schema_module_exec})
19
  #   but the `module` syntax can be more convenient, especially for assigning or accessing constants.
20
  #
21
  # The schema module makes it straightforward to access the schema modules of the schema's subschemas.
22
  # It defines readers for schema properties (keywords) on its singleton (that is,
23
  # called on the module itself, not on instances of it) to access these.
24
  # The {SchemaModule::Connects#[] #[]} method can also be used.
25
  #
26
  # For example, given a schema with an `items` subschema, then `schema.items.jsi_schema_module`
27
  # and `schema.jsi_schema_module.items` both refer to the same module.
28
  # Subscripting with {SchemaModule::Connects#[] #[]} can refer to subschemas on properties
29
  # that can have any name, e.g. `schema.properties['foo'].jsi_schema_module` is the same as
30
  # `schema.jsi_schema_module.properties['foo']`.
31
  #
32
  # Schema module property readers and `#[]` can also take a block, which is passed to `module_exec`.
33
  #
34
  # Putting the above together, here is example usage with the schema module of the Contact
35
  # schema used in the README:
36
  #
37
  # ```ruby
38
  # Contact = JSI.new_schema_module({
39
  #   "$schema" => "http://json-schema.org/draft-07/schema",
40
  #   "type" => "object",
41
  #   "properties" => {
42
  #     "name" => {"type" => "string"},
43
  #     "phone" => {
44
  #       "type" => "array",
45
  #       "items" => {
46
  #         "type" => "object",
47
  #         "properties" => {
48
  #           "location" => {"type" => "string"},
49
  #           "number" => {"type" => "string"}
50
  #         }
51
  #       }
52
  #     }
53
  #   }
54
  # })
55
  #
56
  # module Contact
57
  #   # name a subschema's schema module
58
  #   PhoneNumber = properties['phone'].items
59
  #
60
  #   # open a subschema's schema module to define methods
61
  #   properties['phone'] do
62
  #     def numbers
63
  #       map(&:number)
64
  #     end
65
  #   end
66
  # end
67
  #
68
  # bill = Contact.new_jsi({"name" => "bill", "phone" => [{"location" => "home", "number" => "555"}]})
69
  # #> #{<JSI (Contact)>
70
  # #>   "name" => "bill",
71
  # #>   "phone" => #[<JSI (Contact.properties["phone"])>
72
  # #>     #{<JSI (Contact::PhoneNumber)> "location" => "home", "number" => "555"}
73
  # #>   ],
74
  # #>   "nickname" => "big b"
75
  # #> }
76
  # ```
77
  #
78
  # Note that when `bill` is inspected, schema module names `Contact`, `Contact.properties["phone"]`,
79
  # and `Contact::PhoneNumber` are informatively shown on respective instances.
80
  class SchemaModule < Module
56✔
81
    # @private
82
    def initialize(schema, &block)
56✔
83
      super(&block)
81,294✔
84

85
      @jsi_node = schema
81,294✔
86

87
      schema.jsi_schemas.each do |schema_schema|
81,294✔
88
        extend SchemaClasses.schema_property_reader_module(schema_schema, conflicting_modules: Set[SchemaModule])
391,902✔
89
      end
90
    end
91

92
    # The schema for which this is the JSI Schema Module
93
    # @return [Base + Schema]
94
    def schema
56✔
95
      @jsi_node
142,036✔
96
    end
97

98
    # a URI which refers to the schema. see {Schema#schema_uri}.
99
    # @return (see Schema#schema_uri)
100
    def schema_uri
56✔
101
      schema.schema_uri
×
102
    end
103

104
    # @return [String]
105
    def inspect
56✔
106
      if name_from_ancestor
1,204✔
107
        if schema.schema_absolute_uri
938✔
108
          -"#{name_from_ancestor} <#{schema.schema_absolute_uri}> (JSI Schema Module)"
448✔
109
        else
110
          -"#{name_from_ancestor} (JSI Schema Module)"
490✔
111
        end
112
      else
113
        -"(JSI Schema Module: #{schema.schema_uri || schema.jsi_ptr.uri})"
266✔
114
      end
115
    end
116

117
    def to_s
56✔
118
      inspect
28✔
119
    end
120

121
    # invokes {JSI::Schema#new_jsi} on this module's schema, passing the given parameters.
122
    #
123
    # @return [Base] a JSI whose content comes from the given instance and whose schemas are
124
    #   in-place applicators of this module's schema.
125
    def new_jsi(instance, **kw)
56✔
126
      raise(BlockGivenError) if block_given?
266✔
127
      schema.new_jsi(instance, **kw)
266✔
128
    end
129

130
    # See {Schema#schema_content}
131
    def schema_content
56✔
132
      schema.jsi_node_content
28✔
133
    end
134

135
    # See {Schema#instance_validate}
136
    def instance_validate(instance)
56✔
137
      schema.instance_validate(instance)
×
138
    end
139

140
    # See {Schema#instance_valid?}
141
    def instance_valid?(instance)
56✔
142
      schema.instance_valid?(instance)
×
143
    end
144

145
    # See {Schema#describes_schema!}
146
    def describes_schema!(dialect)
56✔
UNCOV
147
      schema.describes_schema!(dialect)
×
148
    end
149

150
    # @private pending stronger stability of dynamic scope
151
    # See {Schema#with_dynamic_scope_from}
152
    def with_dynamic_scope_from(node)
56✔
153
      node = node.jsi_node if node.is_a?(SchemaModule::Connects)
×
154
      schema.jsi_with_schema_dynamic_anchor_map(node.jsi_next_schema_dynamic_anchor_map).jsi_schema_module
×
155
    end
156

157
    # `$defs` property reader
158
    def defs
56✔
159
      self['$defs']
×
160
    end
161
  end
162

163
  # A module to extend the {SchemaModule} of a schema which describes other schemas (a {Schema::MetaSchema})
164
  module SchemaModule::MetaSchemaModule
56✔
165
    # Instantiates the given schema content as a JSI Schema.
166
    #
167
    # See {JSI::Schema::MetaSchema#new_schema}.
168
    #
169
    # @return [Base + Schema] A JSI which is a {Schema} whose content comes from
170
    #   the given `schema_content` and whose schemas are in-place applicators of this module's schema.
171
    def new_schema(schema_content, **kw, &block)
56✔
172
      schema.new_schema(schema_content, **kw, &block)
3,822✔
173
    end
174

175
    # (see Schema::MetaSchema#new_schema_module)
176
    def new_schema_module(schema_content, **kw, &block)
56✔
177
      schema.new_schema(schema_content, **kw, &block).jsi_schema_module
70✔
178
    end
179

180
    # @return [Schema::Dialect]
181
    def described_dialect
56✔
182
      schema.described_dialect
×
183
    end
184
  end
185

186
  # this module is a namespace for building schema classes and schema modules.
187
  # @private
188
  module SchemaClasses
56✔
189
    class << self
56✔
190
      # @private
191
      # @return [Set<Module>]
192
      def includes_for(instance)
56✔
193
        includes = Set[]
706,728✔
194
        includes << Base::ArrayNode if instance.respond_to?(:to_ary)
706,728✔
195
        includes << Base::HashNode if instance.respond_to?(:to_hash)
706,728✔
196
        includes << Base::StringNode if instance.respond_to?(:to_str)
706,728✔
197
        includes.freeze
706,728✔
198
      end
199

200
      # a JSI Schema Class which represents the given schemas.
201
      # an instance of the class is a JSON Schema instance described by all of the given schemas.
202
      # @api private
203
      # @param schemas [Enumerable<JSI::Schema>] schemas which the class will represent
204
      # @param includes [Enumerable<Module>] modules which will be included on the class
205
      # @return [Class subclass of JSI::Base]
206
      def class_for_schemas(schemas, includes: , mutable: )
56✔
207
        @class_for_schemas_map[
778,574✔
208
          schema_modules: schemas.map(&:jsi_schema_module).to_set.freeze,
209
          includes: includes.to_set.freeze,
210
          mutable: mutable,
211
        ]
212
      end
213

214
      private def class_for_schemas_compute(schema_modules: , includes: , mutable: )
56✔
215
          Class.new(Base) do
74,902✔
216
            schemas = SchemaSet.new(schema_modules.map(&:schema))
74,902✔
217

218
            define_singleton_method(:jsi_class_schemas) { schemas }
76,680✔
219
            define_method(:jsi_schemas) { schemas }
1,343,974✔
220

221
            define_singleton_method(:jsi_class_includes) { includes }
76,666✔
222

223
            mutability_module = mutable ? Base::Mutable : Base::Immutable
74,902✔
224
            conflicting_modules = Set[JSI::Base, mutability_module] + includes + schema_modules
74,902✔
225

226
            include(mutability_module)
74,902✔
227

228
            reader_modules = schemas.map do |schema|
74,902✔
229
              JSI::SchemaClasses.schema_property_reader_module(schema, conflicting_modules: conflicting_modules)
115,572✔
230
            end
231
            reader_modules.each { |m| include m }
190,474✔
232
            readers = reader_modules.map(&:jsi_property_readers).inject(Set[], &:merge).freeze
74,902✔
233
            define_method(:jsi_property_readers) { readers }
120,948✔
234
            define_singleton_method(:jsi_property_readers) { readers }
74,902✔
235

236
            if mutable
74,902✔
237
              writer_modules = schemas.map do |schema|
784✔
238
                JSI::SchemaClasses.schema_property_writer_module(schema, conflicting_modules: conflicting_modules)
1,064✔
239
              end
240
              writer_modules.each { |m| include(m) }
1,848✔
241
            end
242

243
            includes.each { |m| include(m) }
136,268✔
244
            schema_modules.to_a.reverse_each { |m| include(m) }
190,474✔
245
          end
246
      end
247

248
      # a module of readers for described property names of the given schema.
249
      #
250
      # @private
251
      # @param schema [JSI::Schema] a schema for which to define readers for any described property names
252
      # @param conflicting_modules [Enumerable<Module>] an array of modules (or classes) which
253
      #   may be used alongside the accessor module. methods defined by any conflicting_module
254
      #   will not be defined as accessors.
255
      # @return [Module]
256
      def schema_property_reader_module(schema, conflicting_modules: )
56✔
257
        @schema_property_reader_module_map[
681,640✔
258
            schema.described_object_property_names.select do |name|
259
              Util.ok_ruby_method_name?(name) &&
3,902,714✔
260
                !conflicting_modules.any? { |m| m.method_defined?(name) || m.private_method_defined?(name) }
9,567,512✔
261
            end.to_set.freeze
262
        ]
263
      end
264

265
      private def schema_property_reader_module_compute(readers)
56✔
266
          Module.new do
1,722✔
267
            define_singleton_method(:inspect) { -"(JSI Schema Property Reader Module: #{readers.to_a.join(', ')})" }
1,722✔
268

269
            define_singleton_method(:jsi_property_readers) { readers }
194,994✔
270

271
            readers.each do |property_name|
1,722✔
272
              define_method(property_name) do |**kw, &block|
12,950✔
273
                self[property_name, **kw, &block]
228,662✔
274
              end
275
            end
276
          end
277
      end
278

279
      # a module of writers for described property names of the given schema.
280
      # @private
281
      def schema_property_writer_module(schema, conflicting_modules: )
56✔
282
        @schema_property_writer_module_map[
1,216✔
283
            schema.described_object_property_names.select do |name|
284
              writer = "#{name}="
1,246✔
285
              Util.ok_ruby_method_name?(name) &&
1,068✔
286
                !conflicting_modules.any? { |m| m.method_defined?(writer) || m.private_method_defined?(writer) }
5,190✔
287
            end.to_set.freeze
288
        ]
289
      end
290

291
      private def schema_property_writer_module_compute(writers)
56✔
292
          Module.new do
126✔
293
            define_singleton_method(:inspect) { -"(JSI Schema Property Writer Module: #{writers.to_a.join(', ')})" }
126✔
294

295
            define_singleton_method(:jsi_property_writers) { writers }
126✔
296

297
            writers.each do |property_name|
126✔
298
                  define_method("#{property_name}=") do |value|
714✔
299
                    self[property_name] = value
170✔
300
                  end
301
            end
302
          end
303
      end
304
    end
305

306
    @class_for_schemas_map          = Hash.new { |h, k| h[k] = class_for_schemas_compute(**k) }
53,402✔
307
    @schema_property_reader_module_map = Hash.new { |h, k| h[k] = schema_property_reader_module_compute(k) }
1,286✔
308
    @schema_property_writer_module_map = Hash.new { |h, k| h[k] = schema_property_writer_module_compute(k) }
146✔
309
  end
310

311
  # connecting {SchemaModule}s via {SchemaModule::Connection}s
312
  module SchemaModule::Connects
56✔
313
    attr_reader :jsi_node
56✔
314

315
    # a name relative to a named schema module of an ancestor schema.
316
    # for example, if `Foos = JSI::JSONSchemaDraft07.new_schema_module({'items' => {}})`
317
    # then the module `Foos.items` will have a name_from_ancestor of `"Foos.items"`
318
    # @api private
319
    # @return [String, nil]
320
    def name_from_ancestor
56✔
321
      named_ancestor_schema, tokens = named_ancestor_schema_tokens
46,018✔
322
      return nil unless named_ancestor_schema
46,018✔
323

324
      name = named_ancestor_schema.jsi_schema_module_name
42,196✔
325
      ancestor = named_ancestor_schema
42,196✔
326
      tokens.each do |token|
42,196✔
327
        if ancestor.jsi_property_readers.include?(token)
50,022✔
328
          name += ".#{token}"
19,860✔
329
        elsif [String, Numeric, TrueClass, FalseClass, NilClass].any? { |m| token.is_a?(m) }
46,816✔
330
          name += "[#{token.inspect}]"
15,870✔
331
        else
332
          return nil
×
333
        end
334
        ancestor = ancestor[token]
50,022✔
335
      end
336
      name.freeze
42,196✔
337
    end
338

339
    # Subscripting a JSI schema module or a {SchemaModule::Connection} will subscript its node, and
340
    # if the result is a JSI::Schema, return the JSI Schema module of that schema; if it is a JSI::Base,
341
    # return a SchemaModule::Connection; or if it is another value (a simple type), return that value.
342
    #
343
    # @param token [Object]
344
    # @yield If the token identifies a schema and a block is given,
345
    #   it is evaluated in the context of the schema's JSI schema module
346
    #   using [Module#module_exec](https://ruby-doc.org/core/Module.html#method-i-module_exec).
347
    # @return [SchemaModule, SchemaModule::Connection, Object]
348
    def [](token, **kw, &block)
56✔
349
      raise(ArgumentError) unless kw.empty? # TODO remove eventually (keyword argument compatibility)
22,876✔
350
      @jsi_node.jsi_child_ensure_present(token)
22,876✔
351
      sub = @jsi_node[token]
22,820✔
352
      if sub.is_a?(JSI::Schema)
22,820✔
353
        sub.jsi_schema_module_exec(&block) if block
11,676✔
354
        sub.jsi_schema_module
11,676✔
355
      elsif block
11,136✔
356
        raise(BlockGivenError, "block given but token #{token.inspect} does not identify a schema")
×
357
      elsif sub.is_a?(JSI::Base)
11,136✔
358
        SchemaModule::Connection.new(sub)
11,130✔
359
      else
360
        sub
14✔
361
      end
362
    end
363

364
    private
56✔
365

366
    # @return [Array<JSI::Schema, Array>, nil]
367
    def named_ancestor_schema_tokens
56✔
368
      schema_ancestors = @jsi_node.jsi_ancestor_nodes
47,376✔
369
      named_ancestor_schema = schema_ancestors.detect do |jsi|
47,376✔
370
        jsi.is_a?(Schema) && jsi.jsi_schema_module_defined? && jsi.jsi_schema_module_name
107,618✔
371
      end
372
      return nil unless named_ancestor_schema
47,376✔
373
      tokens = @jsi_node.jsi_ptr.relative_to(named_ancestor_schema.jsi_ptr).tokens
43,316✔
374
      [named_ancestor_schema, tokens]
43,316✔
375
    end
376
  end
377

378
  class SchemaModule
56✔
379
    include Connects
56✔
380
  end
381

382
  # A JSI Schema Module is a module which represents a schema. A SchemaModule::Connection represents
383
  # a node in a schema's document which is not a schema, such as the 'properties'
384
  # object (which contains schemas but is not a schema).
385
  #
386
  # instances of this class act as a stand-in to allow users to subscript or call property accessors on
387
  # schema modules to refer to their subschemas' schema modules.
388
  #
389
  # A SchemaModule::Connection has readers for property names described by the node's schemas.
390
  class SchemaModule::Connection
56✔
391
    include SchemaModule::Connects
56✔
392

393
    # @param node [JSI::Base]
394
    def initialize(node)
56✔
395
      fail(Bug, "node must be JSI::Base: #{node.pretty_inspect.chomp}") unless node.is_a?(JSI::Base)
11,130✔
396
      fail(Bug, "node must not be JSI::Schema: #{node.pretty_inspect.chomp}") if node.is_a?(JSI::Schema)
11,130✔
397
      @jsi_node = node
11,130✔
398
      node.jsi_schemas.each do |schema|
11,130✔
399
        extend(JSI::SchemaClasses.schema_property_reader_module(schema, conflicting_modules: [SchemaModule::Connection]))
11,130✔
400
      end
401
    end
402

403
    # @return [String]
404
    def inspect
56✔
405
      if name_from_ancestor
84✔
406
        -"#{name_from_ancestor} (#{self.class})"
42✔
407
      else
408
        -"(#{self.class}: #{@jsi_node.jsi_ptr.uri})"
42✔
409
      end
410
    end
411

412
    def to_s
56✔
413
      inspect
28✔
414
    end
415
  end
416
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc