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

notEthan / jsi / 17544497139

08 Sep 2025 08:21AM UTC coverage: 99.012% (-0.01%) from 99.026%
17544497139

push

github

notEthan
rm SchemaModule::Connects module

2 of 3 new or added lines in 1 file covered. (66.67%)

12 existing lines in 2 files now uncovered.

7416 of 7490 relevant lines covered (99.01%)

138802.19 hits per line

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

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

3
module JSI
56✔
4
  SchemaModule = Class.new(Class.new(Module))
56✔
5
  SchemaModule.const_set(:Connection, SchemaModule.superclass)
56✔
6

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

90
    # a URI which refers to the schema. see {Schema#schema_uri}.
91
    # @return (see Schema#schema_uri)
92
    def schema_uri
56✔
UNCOV
93
      schema.schema_uri
×
94
    end
95

96
    # @return [String]
97
    def inspect
56✔
98
      if name_from_ancestor
1,204✔
99
        if schema.schema_absolute_uri
938✔
100
          -"#{name_from_ancestor} <#{schema.schema_absolute_uri}> (JSI Schema Module)"
448✔
101
        else
102
          -"#{name_from_ancestor} (JSI Schema Module)"
490✔
103
        end
104
      else
105
        -"(JSI Schema Module: #{schema.schema_uri || schema.jsi_ptr.uri})"
266✔
106
      end
107
    end
108

109
    def to_s
56✔
110
      inspect
28✔
111
    end
112

113
    # invokes {JSI::Schema#new_jsi} on this module's schema, passing the given parameters.
114
    #
115
    # @return [Base] a JSI whose content comes from the given instance and whose schemas are
116
    #   in-place applicators of this module's schema.
117
    def new_jsi(instance, **kw)
56✔
118
      raise(BlockGivenError) if block_given?
266✔
119
      schema.new_jsi(instance, **kw)
266✔
120
    end
121

122
    # See {Schema#schema_content}
123
    def schema_content
56✔
124
      schema.jsi_node_content
28✔
125
    end
126

127
    # See {Schema#instance_validate}
128
    def instance_validate(instance)
56✔
UNCOV
129
      schema.instance_validate(instance)
×
130
    end
131

132
    # See {Schema#instance_valid?}
133
    def instance_valid?(instance)
56✔
UNCOV
134
      schema.instance_valid?(instance)
×
135
    end
136

137
    # See {Schema#describes_schema!}
138
    def describes_schema!(dialect)
56✔
UNCOV
139
      schema.describes_schema!(dialect)
×
140
    end
141

142
    # @private pending stronger stability of dynamic scope
143
    # See {Schema#with_dynamic_scope_from}
144
    def with_dynamic_scope_from(node)
56✔
NEW
145
      node = node.jsi_node if node.is_a?(SchemaModule::Connection)
×
UNCOV
146
      schema.jsi_with_schema_dynamic_anchor_map(node.jsi_next_schema_dynamic_anchor_map).jsi_schema_module
×
147
    end
148

149
    # `$defs` property reader
150
    def defs
56✔
UNCOV
151
      self['$defs']
×
152
    end
153
  end
154

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

167
    # (see Schema::MetaSchema#new_schema_module)
168
    def new_schema_module(schema_content, **kw, &block)
56✔
169
      schema.new_schema(schema_content, **kw, &block).jsi_schema_module
70✔
170
    end
171

172
    # @return [Schema::Dialect]
173
    def described_dialect
56✔
UNCOV
174
      schema.described_dialect
×
175
    end
176
  end
177

178
  # this module is a namespace for building schema classes and schema modules.
179
  # @private
180
  module SchemaClasses
56✔
181
    class << self
56✔
182
      # @private
183
      # @return [Set<Module>]
184
      def includes_for(instance)
56✔
185
        includes = Set[]
706,728✔
186
        includes << Base::ArrayNode if instance.respond_to?(:to_ary)
706,728✔
187
        includes << Base::HashNode if instance.respond_to?(:to_hash)
706,728✔
188
        includes << Base::StringNode if instance.respond_to?(:to_str)
706,728✔
189
        includes.freeze
706,728✔
190
      end
191

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

206
      private def class_for_schemas_compute(schema_modules: , includes: , mutable: )
56✔
207
          Class.new(Base) do
74,902✔
208
            schemas = SchemaSet.new(schema_modules.map(&:schema))
74,902✔
209

210
            define_singleton_method(:jsi_class_schemas) { schemas }
76,680✔
211
            define_method(:jsi_schemas) { schemas }
1,341,160✔
212

213
            define_singleton_method(:jsi_class_includes) { includes }
76,666✔
214

215
            mutability_module = mutable ? Base::Mutable : Base::Immutable
74,902✔
216
            conflicting_modules = Set[JSI::Base, mutability_module] + includes + schema_modules
74,902✔
217

218
            include(mutability_module)
74,902✔
219

220
            reader_modules = schemas.map do |schema|
74,902✔
221
              JSI::SchemaClasses.schema_property_reader_module(schema, conflicting_modules: conflicting_modules)
115,572✔
222
            end
223
            reader_modules.each { |m| include m }
190,474✔
224
            readers = reader_modules.map(&:jsi_property_readers).inject(Set[], &:merge).freeze
74,902✔
225
            define_method(:jsi_property_readers) { readers }
120,948✔
226
            define_singleton_method(:jsi_property_readers) { readers }
74,902✔
227

228
            if mutable
74,902✔
229
              writer_modules = schemas.map do |schema|
784✔
230
                JSI::SchemaClasses.schema_property_writer_module(schema, conflicting_modules: conflicting_modules)
1,064✔
231
              end
232
              writer_modules.each { |m| include(m) }
1,848✔
233
            end
234

235
            includes.each { |m| include(m) }
136,268✔
236
            schema_modules.to_a.reverse_each { |m| include(m) }
190,474✔
237
          end
238
      end
239

240
      # a module of readers for described property names of the given schema.
241
      #
242
      # @private
243
      # @param schema [JSI::Schema] a schema for which to define readers for any described property names
244
      # @param conflicting_modules [Enumerable<Module>] an array of modules (or classes) which
245
      #   may be used alongside the accessor module. methods defined by any conflicting_module
246
      #   will not be defined as accessors.
247
      # @return [Module]
248
      def schema_property_reader_module(schema, conflicting_modules: )
56✔
249
        @schema_property_reader_module_map[
670,344✔
250
            schema.described_object_property_names.select do |name|
251
              Util.ok_ruby_method_name?(name) &&
3,902,666✔
252
                !conflicting_modules.any? { |m| m.method_defined?(name) || m.private_method_defined?(name) }
9,567,448✔
253
            end.to_set.freeze
254
        ]
255
      end
256

257
      private def schema_property_reader_module_compute(readers)
56✔
258
          Module.new do
1,722✔
259
            define_singleton_method(:inspect) { -"(JSI Schema Property Reader Module: #{readers.to_a.join(', ')})" }
1,722✔
260

261
            define_singleton_method(:jsi_property_readers) { readers }
194,994✔
262

263
            readers.each do |property_name|
1,722✔
264
              define_method(property_name) do |**kw, &block|
12,950✔
265
                self[property_name, **kw, &block]
228,662✔
266
              end
267
            end
268
          end
269
      end
270

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

283
      private def schema_property_writer_module_compute(writers)
56✔
284
          Module.new do
126✔
285
            define_singleton_method(:inspect) { -"(JSI Schema Property Writer Module: #{writers.to_a.join(', ')})" }
126✔
286

287
            define_singleton_method(:jsi_property_writers) { writers }
126✔
288

289
            writers.each do |property_name|
126✔
290
                  define_method("#{property_name}=") do |value|
714✔
291
                    self[property_name] = value
170✔
292
                  end
293
            end
294
          end
295
      end
296
    end
297

298
    @class_for_schemas_map          = Hash.new { |h, k| h[k] = class_for_schemas_compute(**k) }
53,402✔
299
    @schema_property_reader_module_map = Hash.new { |h, k| h[k] = schema_property_reader_module_compute(k) }
1,286✔
300
    @schema_property_writer_module_map = Hash.new { |h, k| h[k] = schema_property_writer_module_compute(k) }
146✔
301
  end
302

303
  class SchemaModule::Connection
56✔
304
    attr_reader :jsi_node
56✔
305

306
    # a name relative to a named schema module of an ancestor schema.
307
    # for example, if `Foos = JSI::JSONSchemaDraft07.new_schema_module({'items' => {}})`
308
    # then the module `Foos.items` will have a name_from_ancestor of `"Foos.items"`
309
    # @api private
310
    # @return [String, nil]
311
    def name_from_ancestor
56✔
312
      named_ancestor_schema, tokens = named_ancestor_schema_tokens
46,018✔
313
      return nil unless named_ancestor_schema
46,018✔
314

315
      name = named_ancestor_schema.jsi_schema_module_name
42,196✔
316
      ancestor = named_ancestor_schema
42,196✔
317
      tokens.each do |token|
42,196✔
318
        if ancestor.jsi_property_readers.include?(token)
50,022✔
319
          name += ".#{token}"
19,860✔
320
        elsif [String, Numeric, TrueClass, FalseClass, NilClass].any? { |m| token.is_a?(m) }
46,816✔
321
          name += "[#{token.inspect}]"
15,870✔
322
        else
UNCOV
323
          return nil
×
324
        end
325
        ancestor = ancestor[token]
50,022✔
326
      end
327
      name.freeze
42,196✔
328
    end
329

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

355
    # @return [Array<JSI::Schema, Array>, nil]
356
    private def named_ancestor_schema_tokens
56✔
357
      schema_ancestors = @jsi_node.jsi_ancestor_nodes
47,376✔
358
      named_ancestor_schema = schema_ancestors.detect do |jsi|
47,376✔
359
        jsi.is_a?(Schema) && jsi.jsi_schema_module_defined? && jsi.jsi_schema_module_name
107,618✔
360
      end
361
      return nil unless named_ancestor_schema
47,376✔
362
      tokens = @jsi_node.jsi_ptr.relative_to(named_ancestor_schema.jsi_ptr).tokens
43,316✔
363
      [named_ancestor_schema, tokens]
43,316✔
364
    end
365
  end
366

367
  # A JSI Schema Module is a module which represents a schema. A SchemaModule::Connection represents
368
  # a node in a schema's document which is not a schema, such as the 'properties'
369
  # object (which contains schemas but is not a schema).
370
  #
371
  # instances of this class act as a stand-in to allow users to subscript or call property accessors on
372
  # schema modules to refer to their subschemas' schema modules.
373
  #
374
  # A SchemaModule::Connection has readers for property names described by the node's schemas.
375
  #
376
  # This class subclasses Module only so that it can be used as a namespace. No object is ever expected to
377
  # be an instance of a SchemaModule::Connection.
378
  class SchemaModule::Connection < Module
56✔
379
    # @param node [JSI::Base]
380
    def initialize(node)
56✔
381
      fail(Bug, "node must be JSI::Base: #{node.pretty_inspect.chomp}") unless node.is_a?(JSI::Base)
82,540✔
382
      @jsi_node = node
82,540✔
383
      node.jsi_schemas.each do |schema|
82,540✔
384
        extend(JSI::SchemaClasses.schema_property_reader_module(schema, conflicting_modules: [SchemaModule::Connection]))
393,148✔
385
      end
386
    end
387

388
    # @return [String]
389
    def inspect
56✔
390
      if name_from_ancestor
84✔
391
        -"#{name_from_ancestor} (#{self.class})"
42✔
392
      else
393
        -"(#{self.class}: #{@jsi_node.jsi_ptr.uri})"
42✔
394
      end
395
    end
396

397
    def to_s
56✔
398
      inspect
28✔
399
    end
400
  end
401
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