jsi 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ec49b425e6253b4ff1bf14b1d2218fe1bd5c905194d169faab948820a2e17302
4
+ data.tar.gz: 56d79d38177b3272ba094e21def5083896a12c8157346909b739927fbe52e542
5
+ SHA512:
6
+ metadata.gz: cd3d72c29b166c0bb7b1a5970742e5538a79fda1a16359fadb35dd3b6ea65911481f40ab8e4f79ec9da56247f385ee6168ad2aeeaec0f4a6735da7370aba8005
7
+ data.tar.gz: c476fb40d83d9b98568bc067375ddf2f95953fe886284ab889fed11df51b12bd3c8c079bb6dc7e0dec52fa38175e61eb1d784b83d3b707cc984100670f8a592a
data/.simplecov ADDED
@@ -0,0 +1 @@
1
+ SimpleCov.start
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ # v0.0.1
2
+
3
+ - extracted JSI from Scorpio
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Ethan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,160 @@
1
+ # JSI: JSON-Schema Instantiation
2
+
3
+ [![Build Status](https://travis-ci.org/notEthan/jsi.svg?branch=master)](https://travis-ci.org/notEthan/jsi)
4
+ [![Coverage Status](https://coveralls.io/repos/github/notEthan/jsi/badge.svg)](https://coveralls.io/github/notEthan/jsi)
5
+
6
+ JSI represents JSON-schemas as ruby classes, and schema instances as instances of those classes.
7
+
8
+ A JSI class aims to be a fairly unobtrusive wrapper around its instance. It adds accessors for known property names, validation methods, and a few other nice things. Mostly though, you use a JSI as you would use its underlying data.
9
+
10
+ ## Example
11
+
12
+ Words are boring, let's code. Here's a schema in yaml:
13
+
14
+ ```yaml
15
+ description: "A Contact"
16
+ type: "object"
17
+ properties:
18
+ name: {type: "string"}
19
+ phone:
20
+ type: "array"
21
+ items:
22
+ type: "object"
23
+ properties:
24
+ location: {type: "string"}
25
+ number: {type: "string"}
26
+ ```
27
+
28
+ And here's an instance of that schema with JSI:
29
+
30
+ ```ruby
31
+ Contact = JSI.class_for_schema(YAML.load_file('contact.schema.yml'))
32
+ ```
33
+
34
+ This definition gives you not just the Contact class, but classes for the whole nested structure. So, if we construct an instance like:
35
+
36
+ ```ruby
37
+ bill = Contact.new(name: 'bill', phone: [{location: 'home', number: '555'}], nickname: 'big b')
38
+ # => #{<Contact fragment="#">
39
+ # #{<Contact fragment="#">
40
+ # "phone" => #[<JSI::SchemaClasses["594126e3-ea3c-5d9a-b5f4-7423a8701f97#/properties/phone"] fragment="#/phone">
41
+ # #{<JSI::SchemaClasses["594126e3-ea3c-5d9a-b5f4-7423a8701f97#/properties/phone/items"] fragment="#/phone/0"> "location" => "home", "number" => "555"}
42
+ # ],
43
+ # "nickname" => "big b"
44
+ # }
45
+ ```
46
+
47
+ The nested classes can be seen as `JSI::SchemaClasses[schema_id]` where schema_id is a generated value.
48
+
49
+ We get accessors for the Contact:
50
+
51
+ ```ruby
52
+ bill.name
53
+ # => "bill"
54
+ ```
55
+
56
+ but also nested accessors - #phone is an instance of its array-type schema, and each phone item is an instance of another object-type schema with location and number accessors:
57
+
58
+ ```ruby
59
+ bill.phone.map(&:location)
60
+ # => ["home"]
61
+ ```
62
+
63
+ We also get validations, as you'd expect given that's largely what json-schema exists to do:
64
+
65
+ ```ruby
66
+ bill.validate
67
+ # => true
68
+ ```
69
+
70
+ ... and validations on the nested schema instances (#phone here), showing in this example validation failure:
71
+
72
+ ```ruby
73
+ bad = Contact.new(phone: [{number: [5, 5, 5]}])
74
+ # => #{<Contact fragment="#">
75
+ # "phone" => #[<JSI::SchemaClasses["594126e3-ea3c-5d9a-b5f4-7423a8701f97#/properties/phone"] fragment="#/phone">
76
+ # #{<JSI::SchemaClasses["594126e3-ea3c-5d9a-b5f4-7423a8701f97#/properties/phone/items"] fragment="#/phone/0">
77
+ # "number" => #[<JSI::SchemaClasses["594126e3-ea3c-5d9a-b5f4-7423a8701f97#/properties/phone/items/properties/number"] fragment="#/phone/0/number"> 5, 5, 5]
78
+ # }
79
+ # ]
80
+ # }
81
+ bad.phone.fully_validate
82
+ # => ["The property '#/0/number' of type array did not match the following type: string in schema 594126e3-ea3c-5d9a-b5f4-7423a8701f97"]
83
+ ```
84
+
85
+ Since the underlying instance is a ruby hash (json object), we can use it like a hash with #[] or, say, #transform_values:
86
+
87
+ ```ruby
88
+ bill.transform_values(&:size)
89
+ # => {"name" => 4, "phone" => 1, "nickname" => 5}
90
+ bill['nickname']
91
+ # => "big b"
92
+ ```
93
+
94
+ There's plenty more JSI has to offer, but this should give you a pretty good idea of basic usage.
95
+
96
+ ## Terminology
97
+
98
+ - JSI::Base is the base class from which other classes representing JSON-Schemas inherit.
99
+ - a JSI class refers to a class representing a schema, a subclass of JSI::Base.
100
+ - "instance" is a term that is significantly overloaded in this space, so documentation will attempt to be clear what kind of instance is meant:
101
+ - a schema instance refers broadly to a data structure that is described by a json-schema.
102
+ - a JSI instance is a ruby object instantiating a JSI class. it has a method #instance which contains the underlying data.
103
+ - a schema refers to a json-schema. a JSI::Schema represents such a json-schema. a JSI class allow instantiation of such a schema.
104
+
105
+ ## JSI classes
106
+
107
+ A JSI class (that is, subclass of JSI::Base) is a starting point but obviously you want your own methods, so you reopen the class as you would any other. referring back to the Example section above, here's an example:
108
+
109
+ ```ruby
110
+ class Contact
111
+ def full_address
112
+ address.values.join(", ")
113
+ end
114
+ def name
115
+ super + ' esq.'
116
+ end
117
+ def name=(name)
118
+ super(name.chomp(' esq.'))
119
+ end
120
+ end
121
+
122
+ bill.name
123
+ # => "bill esq."
124
+ bill.name = 'rob esq.'
125
+ # => "rob esq."
126
+ bill.instance['name']
127
+ # => "rob"
128
+ ```
129
+
130
+ Note the use of `super` - you can call to accessors defined by JSI and make your accessors act as wrappers (these accessor methods are defined on an included class instead of the JSI class for this reason). You can also use [] and []=, of course, with the same effect.
131
+
132
+ ## ActiveRecord serialization
133
+
134
+ A really excellent place to use JSI is when dealing with serialized columns in ActiveRecord.
135
+
136
+ Let's say you're sticking to json types in the database - you have to do so if you're using json columns, or json serialization, and if you have dealt with arbitrary yaml- or marshal-serialized objects in ruby, you have probably found that approach has its shortcomings when the implementation of your classes changes.
137
+
138
+ But if your database contains json, then your deserialized objects in ruby are likewise Hash / Array / basic types. You have to use subscripts instead of accessors, and you don't have any way to add methods to your data types.
139
+
140
+ JSI gives you the best of both with SchemaInstanceJSONCoder. The objects in your database are simple json types, and your ruby classes are extensible and have the accessors you get from a JSI class hierarchy. Here's an example:
141
+
142
+ ```ruby
143
+ class User < ActiveRecord::Base
144
+ serialize :contacts, JSI::SchemaInstanceJSONCoder.new(Contact, array: true)
145
+ end
146
+ ```
147
+
148
+ Now `user.contacts` will return an array of Contact instances, from the json type in the database, with Contact's accessors, validations, and user-defined instance methods.
149
+
150
+ ## Keying Hashes (JSON Objects)
151
+
152
+ Unlike Ruby, JSON only supports string keys. JSI converts symbols to strings for its internal hash keys (much like ActiveSupport::HashWithIndifferentAccess). JSI accepts symbols to refer to its string hash keys for instantiation, but does not currently transform symbols to strings everywhere else, e.g. `bill[:name]` is `nil` whereas `bill['name']` is `"bill"`.
153
+
154
+ ## Contributing
155
+
156
+ Issues and pull requests are welcome on GitHub at https://github.com/notEthan/jsi.
157
+
158
+ ## License
159
+
160
+ JSI is open source software available under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile.rb ADDED
@@ -0,0 +1,9 @@
1
+ require "rake/testtask"
2
+
3
+ Rake::TestTask.new(:test) do |t|
4
+ t.libs << "test"
5
+ t.libs << "lib"
6
+ t.test_files = FileList["test/**/*_test.rb"]
7
+ end
8
+
9
+ task :default => :test
data/jsi.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "jsi/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "jsi"
7
+ spec.version = JSI::VERSION
8
+ spec.authors = ["Ethan"]
9
+ spec.email = ["ethan@unth"]
10
+
11
+ spec.summary = "JSI: JSON-Schema instantiation"
12
+ spec.description = "JSI represents json-schemas as ruby classes and json-schema instances as instances of those classes"
13
+ spec.homepage = "https://github.com/notEthan/jsi"
14
+ spec.license = "MIT"
15
+ ignore_files = %w(.gitignore .travis.yml Gemfile test)
16
+ ignore_files_re = %r{\A(#{ignore_files.map { |f| Regexp.escape(f) }.join('|')})(/|\z)}
17
+ Dir.chdir(File.expand_path('..', __FILE__)) do
18
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(ignore_files_re) }
19
+ spec.test_files = `git ls-files -z test`.split("\x0")
20
+ end
21
+ spec.bindir = "exe"
22
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
+ spec.require_paths = ["lib"]
24
+
25
+ # we are monkey patching json-schema with a fix that has not been merged in a timely fashion.
26
+ spec.add_dependency "json-schema", "~> 2.8"
27
+ spec.add_development_dependency "rake", "~> 10.0"
28
+ spec.add_development_dependency "minitest", "~> 5.0"
29
+ spec.add_development_dependency "minitest-around"
30
+ spec.add_development_dependency "minitest-reporters"
31
+ end
data/lib/jsi.rb ADDED
@@ -0,0 +1,28 @@
1
+ require "jsi/version"
2
+ require "pp"
3
+ require "jsi/json-schema-fragments"
4
+ require "jsi/util"
5
+
6
+ module JSI
7
+ # generally put in code paths that are not expected to be valid control flow paths.
8
+ # rather a NotImplementedCorrectlyError. but that's too long.
9
+ class Bug < NotImplementedError
10
+ end
11
+
12
+ autoload :JSON, 'jsi/json'
13
+ autoload :Typelike, 'jsi/typelike_modules'
14
+ autoload :Hashlike, 'jsi/typelike_modules'
15
+ autoload :Arraylike, 'jsi/typelike_modules'
16
+ autoload :Schema, 'jsi/schema'
17
+ autoload :Base, 'jsi/base'
18
+ autoload :BaseArray, 'jsi/base'
19
+ autoload :BaseHash, 'jsi/base'
20
+ autoload :SchemaClasses, 'jsi/base'
21
+ autoload :ObjectJSONCoder, 'jsi/schema_instance_json_coder'
22
+ autoload :StructJSONCoder, 'jsi/struct_json_coder'
23
+ autoload :SchemaInstanceJSONCoder,'jsi/schema_instance_json_coder'
24
+
25
+ def self.class_for_schema(*a, &b)
26
+ SchemaClasses.class_for_schema(*a, &b)
27
+ end
28
+ end
data/lib/jsi/base.rb ADDED
@@ -0,0 +1,325 @@
1
+ require 'json'
2
+ require 'jsi/typelike_modules'
3
+
4
+ module JSI
5
+ # base class for representing an instance of an instance described by a schema
6
+ class Base
7
+ include Memoize
8
+ include Enumerable
9
+
10
+ class << self
11
+ def schema_id
12
+ schema.schema_id
13
+ end
14
+
15
+ def inspect
16
+ if !respond_to?(:schema)
17
+ super
18
+ elsif !name || name =~ /\AJSI::SchemaClasses::/
19
+ %Q(#{SchemaClasses.inspect}[#{schema_id.inspect}])
20
+ else
21
+ %Q(#{name} (#{schema_id}))
22
+ end
23
+ end
24
+ def to_s
25
+ if !respond_to?(:schema)
26
+ super
27
+ elsif !name || name =~ /\AJSI::SchemaClasses::/
28
+ %Q(#{SchemaClasses.inspect}[#{schema_id.inspect}])
29
+ else
30
+ name
31
+ end
32
+ end
33
+
34
+ def schema_classes_const_name
35
+ name = schema.schema_id.gsub(/[^\w]/, '_')
36
+ name = 'X' + name unless name[/\A[a-zA-Z_]/]
37
+ name = name[0].upcase + name[1..-1]
38
+ name
39
+ end
40
+
41
+ def name
42
+ unless super
43
+ SchemaClasses.const_set(schema_classes_const_name, self)
44
+ end
45
+ super
46
+ end
47
+ end
48
+
49
+ def initialize(instance, origin: nil)
50
+ unless respond_to?(:schema)
51
+ raise(TypeError, "cannot instantiate #{self.class.inspect} which has no method #schema. please use JSI.class_for_schema")
52
+ end
53
+
54
+ @origin = origin || self
55
+ self.instance = instance
56
+
57
+ if @instance.is_a?(JSI::JSON::HashNode)
58
+ extend BaseHash
59
+ elsif @instance.is_a?(JSI::JSON::ArrayNode)
60
+ extend BaseArray
61
+ end
62
+ end
63
+
64
+ attr_reader :instance
65
+
66
+ # each is overridden by BaseHash or BaseArray when appropriate. the base
67
+ # #each is not actually implemented, along with all the methods of Enumerable.
68
+ def each
69
+ raise NoMethodError, "Enumerable methods and #each not implemented for instance that is not like a hash or array: #{instance.pretty_inspect.chomp}"
70
+ end
71
+
72
+ def parents
73
+ parent = @origin
74
+ (@origin.instance.path.size...self.instance.path.size).map do |i|
75
+ parent.tap do
76
+ parent = parent[self.instance.path[i]]
77
+ end
78
+ end.reverse
79
+ end
80
+ def parent
81
+ parents.first
82
+ end
83
+
84
+ def deref
85
+ derefed = instance.deref
86
+ if derefed.object_id == instance.object_id
87
+ self
88
+ else
89
+ self.class.new(derefed, origin: @origin)
90
+ end
91
+ end
92
+
93
+ def modified_copy(&block)
94
+ modified_instance = instance.modified_copy(&block)
95
+ self.class.new(modified_instance, origin: @origin)
96
+ end
97
+
98
+ def fragment
99
+ instance.fragment
100
+ end
101
+
102
+ def fully_validate
103
+ schema.fully_validate(instance)
104
+ end
105
+ def validate
106
+ schema.validate(instance)
107
+ end
108
+ def validate!
109
+ schema.validate!(instance)
110
+ end
111
+ def inspect
112
+ "\#<#{self.class.to_s} #{instance.inspect}>"
113
+ end
114
+ def pretty_print(q)
115
+ q.instance_exec(self) do |obj|
116
+ text "\#<#{obj.class.to_s}"
117
+ group_sub {
118
+ nest(2) {
119
+ breakable ' '
120
+ pp obj.instance
121
+ }
122
+ }
123
+ breakable ''
124
+ text '>'
125
+ end
126
+ end
127
+
128
+ def object_group_text
129
+ instance.object_group_text
130
+ end
131
+
132
+ def as_json(*opt)
133
+ Typelike.as_json(instance, *opt)
134
+ end
135
+
136
+ def fingerprint
137
+ {class: self.class, instance: instance}
138
+ end
139
+ include FingerprintHash
140
+
141
+ private
142
+ def instance=(thing)
143
+ if instance_variable_defined?(:@instance)
144
+ raise(JSI::Bug, "overwriting instance is not supported")
145
+ end
146
+ if thing.is_a?(Base)
147
+ warn "assigning instance to a Base instance is incorrect. received: #{thing.pretty_inspect.chomp}"
148
+ @instance = JSI.deep_stringify_symbol_keys(thing.instance)
149
+ elsif thing.is_a?(JSI::JSON::Node)
150
+ @instance = JSI.deep_stringify_symbol_keys(thing)
151
+ else
152
+ @instance = JSI::JSON::Node.new_by_type(JSI.deep_stringify_symbol_keys(thing), [])
153
+ end
154
+ end
155
+
156
+ def subscript_assign(subscript, value)
157
+ clear_memo(:[], subscript)
158
+ if value.is_a?(Base)
159
+ instance[subscript] = value.instance
160
+ else
161
+ instance[subscript] = value
162
+ end
163
+ end
164
+ end
165
+
166
+ # this module is just a namespace for schema classes.
167
+ module SchemaClasses
168
+ extend Memoize
169
+ def self.[](schema_id)
170
+ @classes_by_id[schema_id]
171
+ end
172
+ @classes_by_id = {}
173
+ end
174
+
175
+ def SchemaClasses.class_for_schema(schema_object)
176
+ if schema_object.is_a?(JSI::Schema)
177
+ schema__ = schema_object
178
+ else
179
+ schema__ = JSI::Schema.new(schema_object)
180
+ end
181
+
182
+ memoize(:class_for_schema, schema__) do |schema_|
183
+ begin
184
+ begin
185
+ Class.new(Base).instance_exec(schema_) do |schema|
186
+ begin
187
+ include(JSI.module_for_schema(schema))
188
+
189
+ SchemaClasses.instance_exec(self) { |klass| @classes_by_id[klass.schema_id] = klass }
190
+
191
+ self
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
198
+
199
+ def self.module_for_schema(schema_object)
200
+ if schema_object.is_a?(JSI::Schema)
201
+ schema__ = schema_object
202
+ else
203
+ schema__ = JSI::Schema.new(schema_object)
204
+ end
205
+
206
+ memoize(:module_for_schema, schema__) do |schema_|
207
+ Module.new.tap do |m|
208
+ m.instance_exec(schema_) do |schema|
209
+ define_method(:schema) { schema }
210
+ define_singleton_method(:schema) { schema }
211
+ define_singleton_method(:included) do |includer|
212
+ includer.send(:define_singleton_method, :schema) { schema }
213
+ end
214
+
215
+ define_singleton_method(:schema_id) do
216
+ schema.schema_id
217
+ end
218
+ define_singleton_method(:inspect) do
219
+ %Q(#<Module for Schema: #{schema_id}>)
220
+ end
221
+
222
+ if schema.describes_hash?
223
+ instance_method_modules = [m, Base, BaseArray, BaseHash]
224
+ instance_methods = instance_method_modules.map do |mod|
225
+ mod.instance_methods + mod.private_instance_methods
226
+ end.inject(Set.new, &:|)
227
+ accessors_to_define = schema.described_hash_property_names.map(&:to_s) - instance_methods.map(&:to_s)
228
+ accessors_to_define.each do |property_name|
229
+ define_method(property_name) do
230
+ if respond_to?(:[])
231
+ self[property_name]
232
+ else
233
+ raise(NoMethodError, "instance does not respond to []; cannot call reader `#{property_name}' for: #{pretty_inspect.chomp}")
234
+ end
235
+ end
236
+ define_method("#{property_name}=") do |value|
237
+ if respond_to?(:[]=)
238
+ self[property_name] = value
239
+ else
240
+ raise(NoMethodError, "instance does not respond to []=; cannot call writer `#{property_name}=' for: #{pretty_inspect.chomp}")
241
+ end
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end
247
+ end
248
+ end
249
+
250
+ module BaseHash
251
+ # Hash methods
252
+ def each
253
+ return to_enum(__method__) { instance.size } unless block_given?
254
+ instance.each_key { |k| yield(k, self[k]) }
255
+ self
256
+ end
257
+
258
+ def to_hash
259
+ inject({}) { |h, (k, v)| h[k] = v; h }
260
+ end
261
+
262
+ include Hashlike
263
+
264
+ # methods that don't look at the value; can skip the overhead of #[] (invoked by #to_hash)
265
+ SAFE_KEY_ONLY_METHODS.each do |method_name|
266
+ define_method(method_name) { |*a, &b| instance.public_send(method_name, *a, &b) }
267
+ end
268
+
269
+ def [](property_name_)
270
+ memoize(:[], property_name_) do |property_name|
271
+ begin
272
+ property_schema = schema.subschema_for_property(property_name)
273
+ property_schema = property_schema && property_schema.match_to_instance(instance[property_name])
274
+
275
+ if property_schema && instance[property_name].is_a?(JSON::Node)
276
+ JSI.class_for_schema(property_schema).new(instance[property_name], origin: @origin)
277
+ else
278
+ instance[property_name]
279
+ end
280
+ end
281
+ end
282
+ end
283
+ def []=(property_name, value)
284
+ subscript_assign(property_name, value)
285
+ end
286
+ end
287
+
288
+ module BaseArray
289
+ def each
290
+ return to_enum(__method__) { instance.size } unless block_given?
291
+ instance.each_index { |i| yield(self[i]) }
292
+ self
293
+ end
294
+
295
+ def to_ary
296
+ to_a
297
+ end
298
+
299
+ include Arraylike
300
+
301
+ # methods that don't look at the value; can skip the overhead of #[] (invoked by #to_a).
302
+ # we override these methods from Arraylike
303
+ SAFE_INDEX_ONLY_METHODS.each do |method_name|
304
+ define_method(method_name) { |*a, &b| instance.public_send(method_name, *a, &b) }
305
+ end
306
+
307
+ def [](i_)
308
+ memoize(:[], i_) do |i|
309
+ begin
310
+ index_schema = schema.subschema_for_index(i)
311
+ index_schema = index_schema && index_schema.match_to_instance(instance[i])
312
+
313
+ if index_schema && instance[i].is_a?(JSON::Node)
314
+ JSI.class_for_schema(index_schema).new(instance[i], origin: @origin)
315
+ else
316
+ instance[i]
317
+ end
318
+ end
319
+ end
320
+ end
321
+ def []=(i, value)
322
+ subscript_assign(i, value)
323
+ end
324
+ end
325
+ end