ryo.rb 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/specs.yml +23 -0
  3. data/.gitignore +6 -0
  4. data/.gitlab-ci.yml +9 -0
  5. data/.rubocop.yml +56 -0
  6. data/.yardoc-template/default/fulldoc/html/css/0x1eef.css +15 -0
  7. data/.yardoc-template/default/layout/html/setup.rb +5 -0
  8. data/.yardoc-template/default/module/setup.rb +7 -0
  9. data/.yardopts +4 -0
  10. data/Gemfile +5 -0
  11. data/LICENSE.txt +22 -0
  12. data/README.md +373 -0
  13. data/Rakefile +3 -0
  14. data/lib/ryo/basic_object.rb +58 -0
  15. data/lib/ryo/builder.rb +106 -0
  16. data/lib/ryo/enumerable.rb +214 -0
  17. data/lib/ryo/function.rb +68 -0
  18. data/lib/ryo/keywords.rb +67 -0
  19. data/lib/ryo/lazy.rb +4 -0
  20. data/lib/ryo/object.rb +58 -0
  21. data/lib/ryo/reflect.rb +379 -0
  22. data/lib/ryo/version.rb +5 -0
  23. data/lib/ryo.rb +197 -0
  24. data/ryo.rb.gemspec +21 -0
  25. data/share/ryo.rb/examples/1.0_prototypes_point_object.rb +12 -0
  26. data/share/ryo.rb/examples/1.1_prototypes_ryo_fn.rb +14 -0
  27. data/share/ryo.rb/examples/2.0_iteration_each.rb +13 -0
  28. data/share/ryo.rb/examples/2.1_iteration_map.rb +16 -0
  29. data/share/ryo.rb/examples/2.2_iteration_ancestors.rb +13 -0
  30. data/share/ryo.rb/examples/3.0_recursion_ryo_from.rb +13 -0
  31. data/share/ryo.rb/examples/3.1_recursion_ryo_from_with_array.rb +19 -0
  32. data/share/ryo.rb/examples/3.2_recursion_ryo_from_with_openstruct.rb +14 -0
  33. data/share/ryo.rb/examples/4.0_basicobject_ryo_basicobject.rb +12 -0
  34. data/share/ryo.rb/examples/4.1_basicobject_ryo_basicobject_from.rb +13 -0
  35. data/share/ryo.rb/examples/5_collisions_resolution_strategy.rb +8 -0
  36. data/share/ryo.rb/examples/6_beyond_hash_objects.rb +20 -0
  37. data/share/ryo.rb/examples/7_ryo_lazy.rb +14 -0
  38. data/share/ryo.rb/examples/setup.rb +3 -0
  39. data/spec/readme_spec.rb +79 -0
  40. data/spec/ryo_basic_object_spec.rb +60 -0
  41. data/spec/ryo_enumerable_spec.rb +197 -0
  42. data/spec/ryo_keywords_spec.rb +86 -0
  43. data/spec/ryo_object_spec.rb +71 -0
  44. data/spec/ryo_prototypes_spec.rb +45 -0
  45. data/spec/ryo_reflect_spec.rb +175 -0
  46. data/spec/ryo_spec.rb +130 -0
  47. data/spec/setup.rb +5 -0
  48. metadata +173 -0
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # {Ryo::Builder Ryo::Builder} is a module that's used underneath Ryo's public
5
+ # interface when creating instances of {Ryo::Object Ryo::Object}, and
6
+ # {Ryo::BasicObject Ryo::BasicObject}. This module is not intended to be
7
+ # used directly.
8
+ #
9
+ # @api private
10
+ module Ryo::Builder
11
+ ##
12
+ # @param [<Ryo::Object, Ryo::BasicObject>] buildee
13
+ # The class of the object to build.
14
+ #
15
+ # @param [<Hash, Ryo::Object, Ryo::BasicObject, #each_pair>] props
16
+ # A Hash object, an object that implements "#each_pair", or a Ryo object.
17
+ #
18
+ # @param [<Ryo::Object, Ryo::BasicObject>, nil] prototype
19
+ # The prototype, or nil for none.
20
+ #
21
+ # @return [<Ryo::Object, Ryo::BasicObject>]
22
+ # Returns a Ryo object.
23
+ #
24
+ # @note
25
+ # When "props" is given as a Ryo object, a duplicate Ryo object is
26
+ # returned in its place.
27
+ def self.build(buildee, props, prototype = nil)
28
+ if Ryo.ryo?(props)
29
+ build(builedee, Ryo.table_of(props), prototype || Ryo.prototype_of(props))
30
+ else
31
+ ryo = buildee.new
32
+ Ryo.set_prototype_of(ryo, prototype)
33
+ Ryo.set_table_of(ryo, {})
34
+ Ryo.extend!(ryo, Ryo)
35
+ props.each_pair { ryo[_1] = _2 }
36
+ ryo
37
+ end
38
+ end
39
+
40
+ ##
41
+ # Creates a Ryo object by recursively walking a Hash object, or an Array of Hash objects.
42
+ #
43
+ # @example
44
+ # objects = Ryo.from([{x: 0, y: 0}, "foo", {point: {x: 0, y: 0}}])
45
+ # objects[0].x # => 0
46
+ # objects[1] # => "foo"
47
+ # objects[2].point.x # => 0
48
+ #
49
+ # @param buildee (see Ryo::Builder.build)
50
+ #
51
+ # @param [<Hash, Ryo::Object, Ryo::BasicObject, Array, #each_pair, #each> ] props
52
+ # A Hash object, a Ryo object, or an array composed of either Hash / Ryo objects.
53
+ #
54
+ # @param prototype (see Ryo::Builder.build)
55
+ #
56
+ # @return (see Ryo::Builder.build)
57
+ #
58
+ # @note
59
+ # When "props" is given as a Ryo object, a duplicate Ryo object is
60
+ # returned in its place.
61
+ def self.recursive_build(buildee, props, prototype = nil)
62
+ if Ryo.ryo?(props)
63
+ recursive_build(buildee, Ryo.table_of(props), prototype || Ryo.prototype_of(props))
64
+ elsif eachless?(props)
65
+ raise TypeError, "The provided object does not implement #each / #each_pair"
66
+ elsif !props.respond_to?(:each_pair)
67
+ map(props) do
68
+ noop = Ryo.ryo?(_1) || !_1.respond_to?(:each_pair)
69
+ noop ? _1 : recursive_build(buildee, _1)
70
+ end
71
+ else
72
+ visited = {}
73
+ props.each_pair { visited[_1] = map_value(buildee, _2) }
74
+ build(buildee, visited, prototype)
75
+ end
76
+ end
77
+
78
+ ##
79
+ # @private
80
+ def self.map_value(buildee, value)
81
+ if Ryo.ryo?(value) || eachless?(value)
82
+ value
83
+ elsif value.respond_to?(:each_pair)
84
+ recursive_build(buildee, value)
85
+ elsif value.respond_to?(:each)
86
+ map(value) { map_value(buildee, _1) }
87
+ end
88
+ end
89
+ private_class_method :map_value
90
+
91
+ ##
92
+ # @private
93
+ def self.eachless?(value)
94
+ !value.respond_to?(:each) && !value.respond_to?(:each_pair)
95
+ end
96
+ private_class_method :eachless?
97
+
98
+ ##
99
+ # @private
100
+ def self.map(obj)
101
+ ary = []
102
+ obj.each { ary.push(yield(_1)) }
103
+ ary
104
+ end
105
+ private_class_method :map
106
+ end
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # The {Ryo::Enumerable Ryo::Enumerable} module implements methods
5
+ # for iterating through and performing operations on Ryo objects.
6
+ # The methods implemented by this module are available as singleton
7
+ # methods on the {Ryo} module.
8
+ module Ryo::Enumerable
9
+ ##
10
+ # The {#each} methods iterates a Ryo object, and yields a key / value pair.
11
+ # When a block is not given, {#each} returns an Enumerator.
12
+ #
13
+ # @example
14
+ # point_a = Ryo(x: 1, y: 2)
15
+ # point_b = Ryo(y: 1)
16
+ # Ryo.each(point_b) { p [_1, _2] }
17
+ # # ["y", 1]
18
+ # # ["y", 2]
19
+ # # ["x", 1]
20
+ #
21
+ # @param [Ryo::Object, Ryo::BasicObject] ryo
22
+ # A Ryo object.
23
+ #
24
+ # @param ancestors (see #each_ryo)
25
+ #
26
+ # @return [<Enumerator, Array>]
27
+ # Returns an Enumerator when a block is not given, otherwise returns an Array.
28
+ def each(ryo, ancestors: nil)
29
+ return enum_for(:each, ryo) unless block_given?
30
+ each_ryo(ryo, ancestors: ancestors) do |_, key, value|
31
+ yield(key, value)
32
+ end
33
+ end
34
+
35
+ ##
36
+ # The {#each_ryo} method iterates through a Ryo object, and its prototypes.
37
+ # {#each_ryo} yields three parameters: a Ryo object, a key, and a value.
38
+ #
39
+ # @example
40
+ # point_a = Ryo(x: 1, y: 2)
41
+ # point_b = Ryo({y: 3}, point_a)
42
+ # Ryo.each_ryo(point_b) { |ryo, key, value| p [ryo, key, value] }
43
+ # # [point_b, "y", 3]
44
+ # # [point_a, "x", 1]
45
+ # # [point_a, "y", 2]
46
+ #
47
+ # @param [<Ryo::BasicObject, Ryo::Object>] ryo
48
+ # A Ryo object.
49
+ #
50
+ # @param [Integer] ancestors
51
+ # `ancestors` is an integer that determines how far up the prototype chain a
52
+ # {Ryo::Enumerable Ryo::Enumerable} method can go. 0 covers a Ryo object,
53
+ # and none of the prototypes in its prototype chain. 1 covers a Ryo object,
54
+ # and one of the prototypes in its prototype chain - and so on. The default
55
+ # behavior is to traverse the entire prototype chain.
56
+ #
57
+ # @return [<Ryo::BasicObject, Ryo::Object>]
58
+ def each_ryo(ryo, ancestors: nil)
59
+ proto_chain = [ryo, *prototype_chain_of(ryo)]
60
+ ancestors ||= -1
61
+ proto_chain[0..ancestors].each do |ryo|
62
+ properties_of(ryo).each do |key|
63
+ yield(ryo, key, ryo[key])
64
+ end
65
+ end
66
+ ryo
67
+ end
68
+
69
+ ##
70
+ # The {#map} method creates a copy of a Ryo object, and then performs a map operation
71
+ # on the copy and its prototypes.
72
+ #
73
+ # @param (see #map!)
74
+ # @return (see #map!)
75
+ def map(ryo, ancestors: nil, &b)
76
+ map!(Ryo.dup(ryo), ancestors: ancestors, &b)
77
+ end
78
+
79
+ ##
80
+ # The {#map!} method performs an in-place map operation on a Ryo object, and its prototypes.
81
+ #
82
+ # @example
83
+ # point = Ryo(x: 2, y: 4)
84
+ # Ryo.map!(point) { _2 * 2 }
85
+ # [point.x, point.y]
86
+ # # => [4, 8]
87
+ #
88
+ # @param [<Ryo::Object, Ryo::BasicObject>] ryo
89
+ # A Ryo object.
90
+ #
91
+ # @param ancestors (see #each_ryo)
92
+ #
93
+ # @return [<Ryo::Object, Ryo::BasicObject>]
94
+ def map!(ryo, ancestors: nil)
95
+ each_ryo(ryo, ancestors: ancestors) do |ryo, key, value|
96
+ ryo[key] = yield(key, value)
97
+ end
98
+ end
99
+
100
+ ##
101
+ # The {#select} method creates a copy of a Ryo object, and then performs a filter operation
102
+ # on the copy and its prototypes.
103
+ #
104
+ # @param (see #select!)
105
+ # @return (see #select!)
106
+ def select(ryo, ancestors: nil, &b)
107
+ select!(Ryo.dup(ryo), ancestors: ancestors, &b)
108
+ end
109
+
110
+ ##
111
+ # The {#select!} method performs an in-place filter operation on a Ryo object, and
112
+ # its prototypes.
113
+ #
114
+ # @example
115
+ # point_a = Ryo(x: 1, y: 2, z: 3)
116
+ # point_b = Ryo({z: 4}, point_a)
117
+ # Ryo.select!(point_b) { |key, value| %w(x y).include?(key) }
118
+ # [point_b.x, point_b.y, point_b.z]
119
+ # # => [1, 2, nil]
120
+ #
121
+ # @param [<Ryo::Object, Ryo::BasicObject>] ryo
122
+ # A Ryo object.
123
+ #
124
+ # @param ancestors (see #each_ryo)
125
+ #
126
+ # @return [<Ryo::Object, Ryo::BasicObject>]
127
+ def select!(ryo, ancestors: nil)
128
+ each_ryo(ryo, ancestors: ancestors) do |ryo, key, value|
129
+ delete(ryo, key) unless yield(key, value)
130
+ end
131
+ end
132
+
133
+ ##
134
+ # The {#reject} method creates a copy of a Ryo object, and then performs a filter operation
135
+ # on the copy and its prototypes.
136
+ #
137
+ # @param (see #reject!)
138
+ # @return (see #reject!)
139
+ def reject(ryo, ancestors: nil, &b)
140
+ reject!(Ryo.dup(ryo), ancestors: ancestors, &b)
141
+ end
142
+
143
+ ##
144
+ # The {#reject!} method performs an in-place filter operation on a Ryo object, and
145
+ # its prototypes.
146
+ #
147
+ # @example
148
+ # point_a = Ryo(x: 1, y: 2, z: 3)
149
+ # point_b = Ryo({z: 4}, point_a)
150
+ # Ryo.reject!(point_b) { |key, value| value > 2 }
151
+ # [point_b.x, point_b.y, point_b.z]
152
+ # # => [1, 2, nil]
153
+ #
154
+ # @param [<Ryo::Object, Ryo::BasicObject>] ryo
155
+ # A Ryo object.
156
+ #
157
+ # @param ancestors (see #each_ryo)
158
+ #
159
+ # @return [<Ryo::Object, Ryo::BasicObject>]
160
+ def reject!(ryo, ancestors: nil)
161
+ each_ryo(ryo, ancestors: ancestors) do |ryo, key, value|
162
+ delete(ryo, key) if yield(key, value)
163
+ end
164
+ end
165
+
166
+ ##
167
+ # The {#any?} method iterates through a Ryo object, and its prototypes - yielding a
168
+ # key / value pair to a block. If the block ever returns a truthy value, {#any?} will
169
+ # break from the iteration and return true - otherwise false will be returned.
170
+ #
171
+ # @param ancestors (see #each_ryo)
172
+ # @return [Boolean]
173
+ def any?(ryo, ancestors: nil)
174
+ each_ryo(ryo, ancestors: ancestors) do |_, key, value|
175
+ return true if yield(key, value)
176
+ end
177
+ false
178
+ end
179
+
180
+ ##
181
+ # The {#all?} method iterates through a Ryo object, and its prototypes - yielding a
182
+ # key / value pair to a block. If the block ever returns a falsey value, {#all?} will
183
+ # break from the iteration and return false - otherwise true will be returned.
184
+ #
185
+ # @param ancestors (see #each_ryo)
186
+ # @return [Boolean]
187
+ def all?(ryo, ancestors: nil)
188
+ each_ryo(ryo, ancestors: ancestors) do |_, key, value|
189
+ return false unless yield(key, value)
190
+ end
191
+ true
192
+ end
193
+
194
+ ##
195
+ # The {#find} method iterates through a Ryo object, and its prototypes - yielding a
196
+ # key / value pair to a block. If the block ever returns a truthy value, {#find} will
197
+ # break from the iteration and return a Ryo object - otherwise nil will be returned.
198
+ #
199
+ # @example
200
+ # point_a = Ryo(x: 5)
201
+ # point_b = Ryo({y: 10}, point_a)
202
+ # point_c = Ryo({z: 15}, point_b)
203
+ # ryo = Ryo.find(point_c) { |key, value| value == 5 }
204
+ # ryo == point_a # => true
205
+ #
206
+ # @param ancestors (see #each_ryo)
207
+ # @return [<Ryo::Object, Ryo::BasicObject>, nil]
208
+ def find(ryo, ancestors: nil)
209
+ each_ryo(ryo, ancestors: ancestors) do |ryo, key, value|
210
+ return ryo if yield(key, value)
211
+ end
212
+ nil
213
+ end
214
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # The {Ryo::Function Ryo::Function} class represents a Ryo
5
+ # function. The class is usually not used directly but through
6
+ # {Ryo::Keywords#fn Ryo.fn}. A Ryo function has a special relationship
7
+ # with Ryo objects: when a Ryo function is assigned as a property
8
+ # on a Ryo object - the "self" of the function becomes bound to the
9
+ # Ryo object.
10
+ class Ryo::Function
11
+ ##
12
+ # @param [Proc] body
13
+ # The body of the function as a block.
14
+ #
15
+ # @return [Ryo::Function]
16
+ # Returns an instance of {Ryo::Function Ryo::Function}.
17
+ def initialize(&body)
18
+ @body = body
19
+ @ryo = nil
20
+ @to_proc = nil
21
+ end
22
+
23
+ ##
24
+ # @return [<Ryo::Object, Ryo::BasicObject>]
25
+ # Returns the object that the function's "self" is bound to.
26
+ def receiver
27
+ @ryo
28
+ end
29
+ alias_method :self, :receiver
30
+
31
+ ##
32
+ # Change the receiver (self) of the function.
33
+ #
34
+ # @param [<Ryo::Object, Ryo::BasicObject>] ryo
35
+ # A Ryo object.
36
+ #
37
+ # @return [nil]
38
+ def bind!(ryo)
39
+ @ryo = ryo
40
+ @to_proc = nil
41
+ end
42
+
43
+ ##
44
+ # Call the function.
45
+ def call(...)
46
+ to_proc.call(...)
47
+ end
48
+
49
+ ##
50
+ # @return [Proc]
51
+ # Returns the function as a lambda bound to {#receiver}.
52
+ def to_proc
53
+ @to_proc ||= lambda!(@body)
54
+ end
55
+
56
+ private
57
+
58
+ def lambda!(body)
59
+ ryo, lambda = @ryo, nil
60
+ Module.new do
61
+ define_method(:__function__, &body)
62
+ lambda = instance_method(:__function__)
63
+ .bind(ryo)
64
+ .to_proc
65
+ end
66
+ lambda
67
+ end
68
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # The {Ryo::Keywords Ryo::Keywords} module implements Ryo equivalents
5
+ # to some of JavaScript's keywords - for example: the **in** and **delete**
6
+ # operators. The instance methods of this module are available as singleton
7
+ # methods on the {Ryo} module.
8
+ module Ryo::Keywords
9
+ ##
10
+ # @example
11
+ # point = Ryo(x: 0, y: 0, print: Ryo.fn { print x, y, "\n" })
12
+ # point.print.()
13
+ #
14
+ # @param [Proc] b
15
+ # The function's body.
16
+ #
17
+ # @return [Ryo::Function]
18
+ # Returns a Ryo function.
19
+ #
20
+ # @see Ryo::Function Ryo::Function
21
+ def fn(&b)
22
+ Ryo::Function.new(&b)
23
+ end
24
+ alias_method :function, :fn
25
+
26
+ ##
27
+ # Equivalent to JavaScript's **in** operator.
28
+ #
29
+ # @param [<Ryo::Object, Ryo::BasicObject>] ryo
30
+ # A Ryo object.
31
+ #
32
+ # @param [<String, #to_s>] property
33
+ # A property name.
34
+ #
35
+ # @return [Boolean]
36
+ # Returns true when **property** is a member of **ryo**, or its prototype chain.
37
+ def in?(ryo, property)
38
+ return false unless ryo
39
+ property?(ryo, property) || in?(prototype_of(ryo), property)
40
+ end
41
+
42
+ ##
43
+ # The {#delete} method deletes a property from a Ryo object.
44
+ # More or less equivalent to JavaScript's "delete" operator.
45
+ #
46
+ # @note
47
+ # This method does not delete properties from the prototype(s)
48
+ # of a Ryo object. <br>
49
+ # For that - see {Ryo::Reflect#delete! Ryo::Reflect#delete!}.
50
+ #
51
+ # @param [<Ryo::Object, Ryo::BasicObject>] ryo
52
+ # A Ryo object.
53
+ #
54
+ # @param [<String, #to_s>] property
55
+ # A property name.
56
+ #
57
+ # @return [void]
58
+ def delete(ryo, property)
59
+ property = property.to_s
60
+ if property?(ryo, property)
61
+ table_of(ryo).delete(property)
62
+ else
63
+ return if getter_defined?(ryo, property)
64
+ define_method!(ryo, property) { ryo[property] }
65
+ end
66
+ end
67
+ end
data/lib/ryo/lazy.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Ryo::Lazy < Ryo::Function
4
+ end
data/lib/ryo/object.rb ADDED
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # {Ryo::Object Ryo::Object} is a Ryo object and subclass of
5
+ # Ruby's Object class that can be created by using
6
+ # [Ryo()](https://0x1eef.github.io/x/ryo.rb/top-level-namespace.html#Ryo-instance_method),
7
+ # {Ryo.Object Ryo.Object()}, {Ryo::Object.from Ryo::Object.from},
8
+ # or {Ryo::Object.create Ryo::Object.create}.
9
+ class Ryo::Object
10
+ ##
11
+ # @param props (see Ryo::Builder.build)
12
+ # @param prototype (see Ryo::Builder.build)
13
+ #
14
+ # @return [Ryo::Object]
15
+ # Returns an instance of {Ryo::Object Ryo::Object}.
16
+ def self.create(props, prototype = nil)
17
+ Ryo::Builder.build(self, props, prototype)
18
+ end
19
+
20
+ ##
21
+ # Creates a Ryo object by recursively walking a Hash object.
22
+ #
23
+ # @param props (see Ryo::Builder.recursive_build)
24
+ # @param prototype (see Ryo::Builder.recursive_build)
25
+ #
26
+ # @return [Ryo::Object]
27
+ # Returns an instance of {Ryo::Object Ryo::Object}.
28
+ def self.from(props, prototype = nil)
29
+ Ryo::Builder.recursive_build(self, props, prototype)
30
+ end
31
+
32
+ ##
33
+ # Duplicates the internals of a Ryo object.
34
+ #
35
+ # @param [Ryo::Object] ryo
36
+ # A Ryo object.
37
+ #
38
+ # @return [Ryo::Object]
39
+ # Returns a Ryo object.
40
+ def initialize_dup(ryo)
41
+ Ryo.set_table_of(self, Ryo.table_of(ryo).dup)
42
+ Ryo.extend!(self, Ryo)
43
+ end
44
+ end
45
+
46
+ ##
47
+ # @example
48
+ # point = Ryo::Object(x: 0, y: 0)
49
+ # p [point.x, point.y] # => [0, 0]
50
+ #
51
+ # @param props (see Ryo::Builder.build)
52
+ # @param prototype (see Ryo::Builder.build)
53
+ #
54
+ # @return [Ryo::Object]
55
+ # Returns an instance of {Ryo::Object Ryo::Object}.
56
+ def Ryo.Object(props, prototype = nil)
57
+ Ryo::Object.create(props, prototype)
58
+ end