jsi 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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