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

notEthan / jsi / 17486962434

05 Sep 2025 07:39AM UTC coverage: 99.015% (-0.07%) from 99.083%
17486962434

push

github

notEthan
Merge branches 'base.conf', 'bootstrap_no_subclass' and 'misc820' into HEAD

157 of 158 new or added lines in 18 files covered. (99.37%)

32 existing lines in 9 files now uncovered.

7337 of 7410 relevant lines covered (99.01%)

141486.06 hits per line

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

93.29
/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,056✔
84

85
      @jsi_node = schema
81,056✔
86

87
      schema.jsi_schemas.each do |schema_schema|
81,056✔
88
        extend SchemaClasses.schema_property_reader_module(schema_schema, conflicting_modules: Set[SchemaModule])
391,552✔
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
143,184✔
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
      schema.new_jsi(instance, **kw)
266✔
127
    end
128

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

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

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

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

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

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

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

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

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

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

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

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

217
            define_singleton_method(:jsi_class_schemas) { schemas }
76,862✔
218
            define_method(:jsi_schemas) { schemas }
1,343,834✔
219

220
            define_singleton_method(:jsi_class_includes) { includes }
76,848✔
221

222
            mutability_module = mutable ? Base::Mutable : Base::Immutable
75,084✔
223
            conflicting_modules = Set[JSI::Base, mutability_module] + includes + schema_modules
75,084✔
224

225
            include(mutability_module)
75,084✔
226

227
            reader_modules = schemas.map do |schema|
75,084✔
228
              JSI::SchemaClasses.schema_property_reader_module(schema, conflicting_modules: conflicting_modules)
116,930✔
229
            end
230
            reader_modules.each { |m| include m }
192,014✔
231
            readers = reader_modules.map(&:jsi_property_readers).inject(Set[], &:merge).freeze
75,084✔
232
            define_method(:jsi_property_readers) { readers }
121,130✔
233
            define_singleton_method(:jsi_property_readers) { readers }
75,084✔
234

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

242
            includes.each { |m| include(m) }
136,604✔
243
            schema_modules.to_a.reverse_each { |m| include(m) }
192,014✔
244
          end
245
      end
246

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

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

268
            define_singleton_method(:jsi_property_readers) { readers }
195,624✔
269

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

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

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

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

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

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

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

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

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

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

363
    private
56✔
364

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

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

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

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

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

411
    def to_s
56✔
412
      inspect
28✔
413
    end
414
  end
415
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