jsi 0.2.1 → 0.3.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.
@@ -1,80 +1,96 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSI
2
- # this module is just a namespace for schema classes.
3
- module SchemaClasses
4
- # JSI::SchemaClasses[schema_id] returns a class for the schema with the
5
- # given id, the same class as returned from JSI.class_for_schema.
6
- #
7
- # @param schema_id [String] absolute schema id as returned by {JSI::Schema#schema_id}
8
- # @return [Class subclassing JSI::Base] the class for that schema
9
- def self.[](schema_id)
10
- @classes_by_id[schema_id]
4
+ # JSI Schema Modules are extended with JSI::SchemaModule
5
+ module SchemaModule
6
+ # @return [String] absolute schema_id of the schema this module represents.
7
+ # see {Schema#schema_id}.
8
+ def schema_id
9
+ schema.schema_id
10
+ end
11
+
12
+ # @return [String]
13
+ def inspect
14
+ idfrag = schema.schema_id || schema.node_ptr.fragment
15
+ if name
16
+ "#{name} (#{idfrag})"
17
+ else
18
+ "(JSI Schema Module: #{idfrag})"
19
+ end
20
+ end
21
+
22
+ # invokes {JSI::Schema#new_jsi} on this module's schema, passing the given instance.
23
+ # @return [JSI::Base] a JSI whose instance is the given instance
24
+ def new_jsi(instance, *a, &b)
25
+ schema.new_jsi(instance, *a, &b)
11
26
  end
12
- @classes_by_id = {}
27
+ end
13
28
 
29
+ # this module is just a namespace for schema classes.
30
+ module SchemaClasses
14
31
  class << self
15
32
  include Memoize
16
33
 
17
34
  # see {JSI.class_for_schema}
18
35
  def class_for_schema(schema_object)
19
- memoize(:class_for_schema, JSI::Schema.from_object(schema_object)) do |schema_|
20
- Class.new(Base).instance_exec(schema_) do |schema|
36
+ jsi_memoize(:class_for_schema, JSI::Schema.from_object(schema_object)) do |schema|
37
+ Class.new(Base).instance_exec(schema) do |schema|
21
38
  define_singleton_method(:schema) { schema }
22
39
  define_method(:schema) { schema }
23
- include(JSI::SchemaClasses.module_for_schema(schema, conflicting_modules: [Base, BaseArray, BaseHash]))
40
+ include(schema.jsi_schema_module)
24
41
 
25
42
  jsi_class = self
26
43
  define_method(:jsi_class) { jsi_class }
27
44
 
28
- SchemaClasses.instance_exec(self) { |klass| @classes_by_id[klass.schema_id] = klass }
29
-
30
45
  self
31
46
  end
32
47
  end
33
48
  end
34
49
 
35
- # a module for the given schema, with accessor methods for any object
36
- # property names the schema identifies. also has a singleton method
37
- # called #schema to access the {JSI::Schema} this module represents.
38
- #
39
- # accessor methods are defined on these modules so that methods can be
40
- # defined on {JSI.class_for_schema} classes without method redefinition
41
- # warnings. additionally, these overriding instance methods can call
42
- # `super` to invoke the normal accessor behavior.
50
+ # a module for the given schema, with accessor methods for any object property names the schema
51
+ # identifies (see {JSI::Schema#described_object_property_names}).
43
52
  #
44
- # no property names that are the same as existing method names on the JSI
45
- # class will be defined. users should use #[] and #[]= to access properties
46
- # whose names conflict with existing methods.
47
- def SchemaClasses.module_for_schema(schema_object, conflicting_modules: [])
48
- schema__ = JSI::Schema.from_object(schema_object)
49
- memoize(:module_for_schema, schema__, conflicting_modules) do |schema_, conflicting_modules_|
53
+ # defines a singleton method #schema to access the {JSI::Schema} this module represents, and extends
54
+ # the module with {JSI::SchemaModule}.
55
+ def module_for_schema(schema_object)
56
+ schema = JSI::Schema.from_object(schema_object)
57
+ jsi_memoize(:module_for_schema, schema) do |schema|
50
58
  Module.new.tap do |m|
51
- m.instance_exec(schema_) do |schema|
59
+ m.module_eval do
52
60
  define_singleton_method(:schema) { schema }
53
- define_singleton_method(:schema_id) do
54
- schema.schema_id
55
- end
56
- define_singleton_method(:inspect) do
57
- %Q(#<Module for Schema: #{schema_id}>)
58
- end
59
61
 
60
- conflicting_instance_methods = (conflicting_modules_ + [m]).map do |mod|
62
+ extend SchemaModule
63
+
64
+ include JSI::SchemaClasses.accessor_module_for_schema(schema, conflicting_modules: [JSI::Base, JSI::BaseArray, JSI::BaseHash])
65
+
66
+ @possibly_schema_node = schema
67
+ extend(SchemaModulePossibly)
68
+ extend(JSI::SchemaClasses.accessor_module_for_schema(schema.schema, conflicting_modules: [Module, SchemaModule, SchemaModulePossibly]))
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ # @param schema [JSI::Schema] a schema for which to define accessors for any described property names
75
+ # @param conflicting_modules [Enumerable<Module>] an array of modules (or classes) which
76
+ # may be used alongside the accessor module. methods defined by any conflicting_module
77
+ # will not be defined as accessors.
78
+ # @return [Module] a module of accessors (setters and getters) for described property names of the given
79
+ # schema
80
+ def accessor_module_for_schema(schema, conflicting_modules: )
81
+ jsi_memoize(:accessor_module_for_schema, schema, conflicting_modules) do |schema, conflicting_modules|
82
+ Module.new.tap do |m|
83
+ m.module_eval do
84
+ conflicting_instance_methods = (conflicting_modules + [m]).map do |mod|
61
85
  mod.instance_methods + mod.private_instance_methods
62
86
  end.inject(Set.new, &:|)
63
87
  accessors_to_define = schema.described_object_property_names.map(&:to_s) - conflicting_instance_methods.map(&:to_s)
64
88
  accessors_to_define.each do |property_name|
65
89
  define_method(property_name) do
66
- if respond_to?(:[])
67
- self[property_name]
68
- else
69
- raise(NoMethodError, "schema instance of class #{self.class} does not respond to []; cannot call reader '#{property_name}'. instance is #{instance.pretty_inspect.chomp}")
70
- end
90
+ self[property_name]
71
91
  end
72
92
  define_method("#{property_name}=") do |value|
73
- if respond_to?(:[]=)
74
- self[property_name] = value
75
- else
76
- raise(NoMethodError, "schema instance of class #{self.class} does not respond to []=; cannot call writer '#{property_name}='. instance is #{instance.pretty_inspect.chomp}")
77
- end
93
+ self[property_name] = value
78
94
  end
79
95
  end
80
96
  end
@@ -83,4 +99,53 @@ module JSI
83
99
  end
84
100
  end
85
101
  end
102
+
103
+ # a JSI::Schema module and a JSI::NotASchemaModule are both a SchemaModulePossibly.
104
+ # this module provides a #[] method.
105
+ module SchemaModulePossibly
106
+ attr_reader :possibly_schema_node
107
+
108
+ # subscripting a JSI schema module or a NotASchemaModule will subscript the node, and
109
+ # if the result is a JSI::Schema, return a JSI::Schema class; if it is a PathedNode,
110
+ # return a NotASchemaModule; or if it is another value (a basic type), return that value.
111
+ #
112
+ # @param token [Object]
113
+ # @return [Class, NotASchemaModule, Object]
114
+ def [](token)
115
+ sub = @possibly_schema_node[token]
116
+ if sub.is_a?(JSI::Schema)
117
+ sub.jsi_schema_module
118
+ elsif sub.is_a?(JSI::PathedNode)
119
+ NotASchemaModule.new(sub)
120
+ else
121
+ sub
122
+ end
123
+ end
124
+ end
125
+
126
+ # a schema module is a module which represents a schema. a NotASchemaModule represents
127
+ # a node in a schema's document which is not a schema, such as the 'properties'
128
+ # node (which contains schemas but is not a schema).
129
+ #
130
+ # a NotASchemaModule is extended with the module_for_schema of the node's schema.
131
+ #
132
+ # NotASchemaModule holds a node which is not a schema. when subscripted, it subscripts
133
+ # its node. if the value is a JSI::Schema, its schema module is returned. if the value
134
+ # is another node, a NotASchemaModule for that node is returned. otherwise - when the
135
+ # value is a basic type - that value itself is returned.
136
+ class NotASchemaModule
137
+ # @param node [JSI::PathedNode]
138
+ def initialize(node)
139
+ unless node.is_a?(JSI::PathedNode)
140
+ raise(TypeError, "not JSI::PathedNode: #{node.pretty_inspect.chomp}")
141
+ end
142
+ if node.is_a?(JSI::Schema)
143
+ raise(TypeError, "cannot instantiate NotASchemaModule for a JSI::Schema node: #{node.pretty_inspect.chomp}")
144
+ end
145
+ @possibly_schema_node = node
146
+ extend(JSI::SchemaClasses.accessor_module_for_schema(node.schema, conflicting_modules: [NotASchemaModule, SchemaModulePossibly]))
147
+ end
148
+
149
+ include SchemaModulePossibly
150
+ end
86
151
  end
@@ -1,7 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSI
2
- SimpleWrap = JSI.class_for_schema({"additionalProperties": {"$ref": "#"}, "items": {"$ref": "#"}})
4
+ SimpleWrap = JSI::Schema.new({
5
+ "additionalProperties": {"$ref": "#"},
6
+ "items": {"$ref": "#"}
7
+ }).jsi_schema_module
3
8
 
4
- # SimpleWrap is a JSI class which recursively wraps nested structures
5
- class SimpleWrap
9
+ # SimpleWrap is a JSI schema module which recursively wraps nested structures
10
+ module SimpleWrap
6
11
  end
7
12
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSI
2
4
  # a module relating to objects that act like Hash or Array instances
3
5
  module Typelike
@@ -136,36 +138,31 @@ module JSI
136
138
  # @return [String] basically the same #inspect as Hash, but has the
137
139
  # class name and, if responsive, self's #object_group_text
138
140
  def inspect
139
- object_group_str = JSI.object_group_str(respond_to?(:object_group_text) ? self.object_group_text : [])
140
- "\#{<#{self.class}#{object_group_str}>#{empty? ? '' : ' '}#{self.map { |k, v| "#{k.inspect} => #{v.inspect}" }.join(', ')}}"
141
+ object_group_str = (respond_to?(:object_group_text) ? self.object_group_text : [self.class]).join(' ')
142
+ "\#{<#{object_group_str}>#{empty? ? '' : ' '}#{self.map { |k, v| "#{k.inspect} => #{v.inspect}" }.join(', ')}}"
141
143
  end
142
144
 
143
- # @return [String] see #inspect
144
- def to_s
145
- inspect
146
- end
145
+ alias_method :to_s, :inspect
147
146
 
148
147
  # pretty-prints a representation this node to the given printer
149
148
  # @return [void]
150
149
  def pretty_print(q)
151
- q.instance_exec(self) do |obj|
152
- object_group_str = JSI.object_group_str(obj.respond_to?(:object_group_text) ? obj.object_group_text : [])
153
- text "\#{<#{obj.class}#{object_group_str}>"
154
- group_sub {
155
- nest(2) {
156
- breakable(obj.any? { true } ? ' ' : '')
157
- seplist(obj, nil, :each_pair) { |k, v|
158
- group {
159
- pp k
160
- text ' => '
161
- pp v
162
- }
150
+ object_group_str = (respond_to?(:object_group_text) ? object_group_text : [self.class]).join(' ')
151
+ q.text "\#{<#{object_group_str}>"
152
+ q.group_sub {
153
+ q.nest(2) {
154
+ q.breakable(any? { true } ? ' ' : '')
155
+ q.seplist(self, nil, :each_pair) { |k, v|
156
+ q.group {
157
+ q.pp k
158
+ q.text ' => '
159
+ q.pp v
163
160
  }
164
161
  }
165
162
  }
166
- breakable ''
167
- text '}'
168
- end
163
+ }
164
+ q.breakable ''
165
+ q.text '}'
169
166
  end
170
167
  end
171
168
 
@@ -217,32 +214,27 @@ module JSI
217
214
  # @return [String] basically the same #inspect as Array, but has the
218
215
  # class name and, if responsive, self's #object_group_text
219
216
  def inspect
220
- object_group_str = JSI.object_group_str(respond_to?(:object_group_text) ? object_group_text : [])
221
- "\#[<#{self.class}#{object_group_str}>#{empty? ? '' : ' '}#{self.map { |e| e.inspect }.join(', ')}]"
217
+ object_group_str = (respond_to?(:object_group_text) ? object_group_text : [self.class]).join(' ')
218
+ "\#[<#{object_group_str}>#{empty? ? '' : ' '}#{self.map { |e| e.inspect }.join(', ')}]"
222
219
  end
223
220
 
224
- # @return [String] see #inspect
225
- def to_s
226
- inspect
227
- end
221
+ alias_method :to_s, :inspect
228
222
 
229
223
  # pretty-prints a representation this node to the given printer
230
224
  # @return [void]
231
225
  def pretty_print(q)
232
- q.instance_exec(self) do |obj|
233
- object_group_str = JSI.object_group_str(obj.respond_to?(:object_group_text) ? obj.object_group_text : [])
234
- text "\#[<#{obj.class}#{object_group_str}>"
235
- group_sub {
236
- nest(2) {
237
- breakable(obj.any? { true } ? ' ' : '')
238
- seplist(obj, nil, :each) { |e|
239
- pp e
240
- }
226
+ object_group_str = (respond_to?(:object_group_text) ? object_group_text : [self.class]).join(' ')
227
+ q.text "\#[<#{object_group_str}>"
228
+ q.group_sub {
229
+ q.nest(2) {
230
+ q.breakable(any? { true } ? ' ' : '')
231
+ q.seplist(self, nil, :each) { |e|
232
+ q.pp e
241
233
  }
242
234
  }
243
- breakable ''
244
- text ']'
245
- end
235
+ }
236
+ q.breakable ''
237
+ q.text ']'
246
238
  end
247
239
  end
248
240
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSI
2
4
  module Util
3
5
  # a proc which does nothing
@@ -17,68 +19,44 @@ module JSI
17
19
  # @param hash [#to_hash] the hash from which to convert symbol keys to strings
18
20
  # @return [same class as the param `hash`, or Hash if the former cannot be done] a
19
21
  # hash(-like) instance containing no symbol keys
20
- def stringify_symbol_keys(hash)
21
- unless hash.respond_to?(:to_hash)
22
- raise(ArgumentError, "expected argument to be a hash; got #{hash.class.inspect}: #{hash.pretty_inspect.chomp}")
22
+ def stringify_symbol_keys(hashlike)
23
+ unless hashlike.respond_to?(:to_hash)
24
+ raise(ArgumentError, "expected argument to be a hash; got #{hashlike.class.inspect}: #{hashlike.pretty_inspect.chomp}")
23
25
  end
24
- JSI::Typelike.modified_copy(hash) do |hash_|
25
- changed = false
26
+ JSI::Typelike.modified_copy(hashlike) do |hash|
26
27
  out = {}
27
- hash_.each do |k, v|
28
- if k.is_a?(Symbol)
29
- changed = true
30
- k = k.to_s
31
- end
32
- out[k] = v
28
+ hash.each do |k, v|
29
+ out[k.is_a?(Symbol) ? k.to_s : k] = v
33
30
  end
34
- changed ? out : hash_
31
+ out
35
32
  end
36
33
  end
37
34
 
38
35
  def deep_stringify_symbol_keys(object)
39
36
  if object.respond_to?(:to_hash)
40
37
  JSI::Typelike.modified_copy(object) do |hash|
41
- changed = false
42
38
  out = {}
43
39
  (hash.respond_to?(:each) ? hash : hash.to_hash).each do |k, v|
44
- if k.is_a?(Symbol)
45
- changed = true
46
- k = k.to_s
47
- end
48
- out_k = deep_stringify_symbol_keys(k)
49
- out_v = deep_stringify_symbol_keys(v)
50
- changed = true if out_k.object_id != k.object_id
51
- changed = true if out_v.object_id != v.object_id
52
- out[out_k] = out_v
40
+ out[k.is_a?(Symbol) ? k.to_s : deep_stringify_symbol_keys(k)] = deep_stringify_symbol_keys(v)
53
41
  end
54
- changed ? out : hash
42
+ out
55
43
  end
56
44
  elsif object.respond_to?(:to_ary)
57
45
  JSI::Typelike.modified_copy(object) do |ary|
58
- changed = false
59
- out = (ary.respond_to?(:each) ? ary : ary.to_ary).map do |e|
60
- out_e = deep_stringify_symbol_keys(e)
61
- changed = true if out_e.object_id != e.object_id
62
- out_e
46
+ (ary.respond_to?(:each) ? ary : ary.to_ary).map do |e|
47
+ deep_stringify_symbol_keys(e)
63
48
  end
64
- changed ? out : ary
65
49
  end
66
50
  else
67
51
  object
68
52
  end
69
53
  end
70
54
 
71
- # @param object_group_text [Array<String>]
72
- # @return [String]
73
- def object_group_str(object_group_text)
74
- object_group_text.compact.map { |t| " #{t}" }.join('')
75
- end
76
-
77
55
  # this is the Y-combinator, which allows anonymous recursive functions. for a simple example,
78
56
  # to define a recursive function to return the length of an array:
79
57
  #
80
58
  # length = ycomb do |len|
81
- # proc{|list| list == [] ? 0 : 1 + len.call(list[1..-1]) }
59
+ # proc { |list| list == [] ? 0 : 1 + len.call(list[1..-1]) }
82
60
  # end
83
61
  #
84
62
  # see https://secure.wikimedia.org/wikipedia/en/wiki/Fixed_point_combinator#Y_combinator
@@ -92,33 +70,35 @@ module JSI
92
70
  extend Util
93
71
 
94
72
  module FingerprintHash
73
+ # overrides BasicObject#==
95
74
  def ==(other)
96
- object_id == other.object_id || (other.respond_to?(:fingerprint) && other.fingerprint == self.fingerprint)
75
+ object_id == other.object_id || (other.respond_to?(:jsi_fingerprint) && other.jsi_fingerprint == self.jsi_fingerprint)
97
76
  end
98
77
 
99
78
  alias_method :eql?, :==
100
79
 
80
+ # overrides Kernel#hash
101
81
  def hash
102
- fingerprint.hash
82
+ jsi_fingerprint.hash
103
83
  end
104
84
  end
105
85
 
106
86
  module Memoize
107
- def memoize(key, *args_)
108
- @memos ||= {}
109
- @memos[key] ||= Hash.new do |h, args|
87
+ def jsi_memoize(key, *args_)
88
+ @jsi_memos ||= {}
89
+ @jsi_memos[key] ||= Hash.new do |h, args|
110
90
  h[args] = yield(*args)
111
91
  end
112
- @memos[key][args_]
92
+ @jsi_memos[key][args_]
113
93
  end
114
94
 
115
- def clear_memo(key, *args)
116
- @memos ||= {}
117
- if @memos[key]
95
+ def jsi_clear_memo(key, *args)
96
+ @jsi_memos ||= {}
97
+ if @jsi_memos[key]
118
98
  if args.empty?
119
- @memos[key].clear
99
+ @jsi_memos[key].clear
120
100
  else
121
- @memos[key].delete(args)
101
+ @jsi_memos[key].delete(args)
122
102
  end
123
103
  end
124
104
  end