yasl 0.1.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ede09b21493ce4855f31b11dace169aeb8a218d2f524c86758ca5643fcfe29e5
4
+ data.tar.gz: 9c34b64eea2244ba1673a2d643872832d694f07c6b644a413824607c9be6469b
5
+ SHA512:
6
+ metadata.gz: 6afd16380e645f3b972508747b3e41c1254058e25f2f55ab7a872140b4f06aa566a165687c162777c6dcae470d4a1b91aea25a5e82d321ae720644a4b927b353
7
+ data.tar.gz: f976082dea29c4b130bb6394cc7f1d7a6d71bbef708b9312d61bad265a62e6e6519c12159a6667dcf50618475fdd5474542d959cb415e0984e7ed14355d4946e
@@ -0,0 +1,23 @@
1
+ # Change Log
2
+
3
+ ## 0.1.0
4
+
5
+ - Serialize JSON basic data types
6
+ - Serialize Ruby basic data types
7
+ - Serialize instance variables as JSON
8
+ - Serialize class variables as JSON
9
+ - Serialize struct member values as JSON
10
+ - Serialize top-level class/module as JSON
11
+ - Serialize cycles by using object ID references
12
+ - Support `include_classes` option on dump
13
+ - Silently ignore non-serializable objects like `Proc`, `Binding`, and `IO`.
14
+ - Deserialize instance variables from JSON
15
+ - Deserialize Class occurence in variables from JSON
16
+ - Deserialize Module occurence in variables from JSON
17
+ - Deserialize class variables from JSON
18
+ - Deserialize Struct members from JSON
19
+ - Deserialize cycles with object ID references
20
+ - Deserialize top-level class/module from JSON
21
+ - Support `include_classes` option on load
22
+ - Raise error for deserialization not finding a class mentioned in the data
23
+ - Require passing `whitelist_classes` to `YASL#load` or else raise error for illegal classes
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2020 Andy Maleh
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,333 @@
1
+ # YASL - Yet Another Serialization Library
2
+ [![Gem Version](https://badge.fury.io/rb/yasl.svg)](http://badge.fury.io/rb/yasl)
3
+ [![Ruby](https://github.com/AndyObtiva/yasl/workflows/Ruby/badge.svg)](https://github.com/AndyObtiva/yasl/actions?query=workflow%3ARuby)
4
+ [![Coverage Status](https://coveralls.io/repos/github/AndyObtiva/yasl/badge.svg?branch=master)](https://coveralls.io/github/AndyObtiva/yasl?branch=master)
5
+ [![Maintainability](https://api.codeclimate.com/v1/badges/e8d043b8c78c801f0aa3/maintainability)](https://codeclimate.com/github/AndyObtiva/yasl/maintainability)
6
+
7
+ A pure Ruby serialization library that works across different Ruby implementations like Opal and JRuby as an alternative to YAML/Marshal.
8
+
9
+ ## Requirements
10
+
11
+ - Portablity across different Ruby implementations, especially Opal and JRuby.
12
+ - Zero configuration. Developers are too busy solving business domain problems to worry about low-level serialization details.
13
+ - Silently ignore non-serializable objects like `Proc`, `Binding`, and `IO`.
14
+ - Support serializing classes and modules, not just object instances.
15
+ - JSON encoding is good enough. No need for premature optimization.
16
+
17
+ ## Usage Instructions
18
+
19
+ Run:
20
+
21
+ `gem install yasl`
22
+
23
+ Or add to Gemfile:
24
+
25
+ ```ruby
26
+ gem 'yasl', '~> 0.1.0'
27
+ ```
28
+
29
+ And, run:
30
+
31
+ `bundle`
32
+
33
+ Finally, require in Ruby code:
34
+
35
+ ```ruby
36
+ require 'yasl'
37
+ ```
38
+
39
+ ### Serialize
40
+
41
+ To serialize, use the `YASL#dump(object)` method.
42
+
43
+ Keep in mind that `YASL::UNSERIALIZABLE_DATA_TYPES` classes are unserializable, and will serialize as `nil` (feel free to add more classes that you would like filtered out):
44
+
45
+ `Proc`, `Binding`, `IO`, `File::Stat`, `Dir`, `BasicSocket`, `MatchData`, `Method`, `UnboundMethod`, `Thread`, `ThreadGroup`, `Continuation`
46
+
47
+ Example (from [samples/dump_basic.rb](samples/dump_basic.rb)):
48
+
49
+ ```ruby
50
+ require 'yasl'
51
+ require 'date'
52
+
53
+ class Car
54
+ attr_accessor :make,
55
+ :model,
56
+ :year,
57
+ :registration_time,
58
+ :registration_date,
59
+ :registration_date_time,
60
+ :complex_number,
61
+ :complex_polar_number,
62
+ :rational_number
63
+ end
64
+
65
+ car = Car.new
66
+ car.make = 'Mitsubishi'
67
+ car.model = 'Eclipse'
68
+ car.year = '2002'
69
+ car.registration_time = Time.new(2003, 10, 19, 10, 39, 37.092, '-03:00')
70
+ car.registration_date = Date.new(2003, 10, 19)
71
+ car.registration_date_time = DateTime.new(2003, 10, 19, 10, 39, 37.092, '-03:00')
72
+ car.complex_number = Complex(2,37)
73
+ car.complex_polar_number = Complex.polar(-23,28)
74
+ car.rational_number = Rational(22/7)
75
+
76
+ dump = YASL.dump(car)
77
+
78
+ puts dump.inspect
79
+
80
+ # => "{\"_class\":\"Car\",\"_id\":1,\"_instance_variables\":{\"make\":\"Mitsubishi\",\"model\":\"Eclipse\",\"year\":\"2002\",\"registration_time\":{\"_class\":\"Time\",\"_data\":[0,2452932,49177,\"12644383719423828125/137438953472\",-10800,2299161.0]},\"registration_date\":{\"_class\":\"Date\",\"_data\":[0,2452932,0,0,0,2299161.0]},\"registration_date_time\":{\"_class\":\"DateTime\",\"_data\":[0,2452932,49177,92000000,-10800,2299161.0]},\"complex_number\":{\"_class\":\"Complex\",\"_data\":\"2+37i\"},\"complex_polar_number\":{\"_class\":\"Complex\",\"_data\":\"22.13993492521203-6.230833131080988i\"},\"rational_number\":{\"_class\":\"Rational\",\"_data\":\"3/1\"}}}"
81
+ ```
82
+
83
+ #### Cycles
84
+
85
+ YASL automatically detects cycles when serializing bidirectional object references.
86
+
87
+ Example (from [samples/dump_cycle.rb](samples/dump_cycle.rb)):
88
+
89
+ ```ruby
90
+ require 'yasl'
91
+ require 'date'
92
+ require 'set'
93
+
94
+ class Car
95
+ attr_accessor :make,
96
+ :model,
97
+ :year,
98
+ :owner
99
+ end
100
+
101
+ class Person
102
+ class << self
103
+ def reset_count!
104
+ @count = 0
105
+ end
106
+
107
+ def increment_count!
108
+ @count ||= 0
109
+ @count += 1
110
+ end
111
+
112
+ def reset_class_count!
113
+ @@class_count = 0
114
+ end
115
+
116
+ def increment_class_count!
117
+ @@class_count = 0 unless defined?(@@class_count)
118
+ @@class_count += 1
119
+ end
120
+ end
121
+
122
+ attr_accessor :name, :dob, :cars
123
+
124
+ def initialize
125
+ self.class.increment_count!
126
+ self.class.increment_class_count!
127
+ end
128
+ end
129
+
130
+ person = Person.new
131
+ person.name = 'Sean Hux'
132
+ person.dob = Time.new(2017, 10, 17, 10, 3, 4)
133
+
134
+ car = Car.new
135
+ car.make = 'Mitsubishi'
136
+ car.model = 'Eclipse'
137
+ car.year = '2002'
138
+
139
+ car.owner = person
140
+ person.cars = [car]
141
+
142
+ dump = YASL.dump(car)
143
+
144
+ puts dump.inspect
145
+
146
+ # => "{\"_class\":\"Car\",\"_id\":1,\"_instance_variables\":{\"make\":\"Mitsubishi\",\"model\":\"Eclipse\",\"owner\":{\"_class\":\"Person\",\"_id\":1,\"_instance_variables\":{\"cars\":{\"_class\":\"Array\",\"_data\":[{\"_class\":\"Car\",\"_id\":1}]},\"dob\":{\"_class\":\"Time\",\"_data\":[0,2458044,50584,0,-14400,2299161.0]},\"name\":\"Sean Hux\"}},\"year\":\"2002\"}}"
147
+ ```
148
+
149
+ ### Deserialize
150
+
151
+ To deserialize, use the `YASL#load(data, whitelist_classes: [])` method. The value of `whitelist_classes` must mention all classes expected to appear in the serialized data to load. This is required to ensure software security by not allowing arbitrary unexpected classes to be deserialized.
152
+
153
+ By default, only `YASL::RUBY_BASIC_DATA_TYPES` classes are deserialized:
154
+
155
+ `NilClass`, `String`, `Integer`, `Float`, `TrueClass`, `FalseClass`, `Time`, `Date`, `Complex`, `Rational`, `Regexp`, `Symbol`, `Set`, `Range`, `Array`, `Hash`
156
+
157
+ Example (from [samples/load_basic.rb](samples/load_basic.rb)):
158
+
159
+ ```ruby
160
+ require 'yasl'
161
+ require 'date'
162
+
163
+ class Car
164
+ attr_accessor :make,
165
+ :model,
166
+ :year,
167
+ :registration_time,
168
+ :registration_date,
169
+ :registration_date_time,
170
+ :complex_number,
171
+ :complex_polar_number,
172
+ :rational_number
173
+ end
174
+
175
+ car = Car.new
176
+ car.make = 'Mitsubishi'
177
+ car.model = 'Eclipse'
178
+ car.year = '2002'
179
+ car.registration_time = Time.new(2003, 10, 19, 10, 39, 37.092, '-03:00')
180
+ car.registration_date = Date.new(2003, 10, 19)
181
+ car.registration_date_time = DateTime.new(2003, 10, 19, 10, 39, 37.092, '-03:00')
182
+ car.complex_number = Complex(2,37)
183
+ car.complex_polar_number = Complex.polar(-23,28)
184
+ car.rational_number = Rational(22/7)
185
+
186
+ dump = YASL.dump(car)
187
+ car2 = YASL.load(dump, whitelist_classes: [Car])
188
+
189
+ puts car2.make
190
+ # => Mitsubishi
191
+
192
+ puts car2.model
193
+ # => Eclipse
194
+
195
+ puts car2.year
196
+ # => 2002
197
+
198
+ puts car2.registration_time
199
+ # => 2003-10-19 10:39:37 -0300
200
+
201
+ puts car2.registration_date
202
+ # => 2003-10-19
203
+
204
+ puts car2.registration_date_time
205
+ # => 2003-10-19T10:39:37-03:00
206
+
207
+ puts car2.complex_number
208
+ # => 2+37i
209
+
210
+ puts car2.complex_polar_number
211
+ # => 22.13993492521203-6.230833131080988i
212
+
213
+ puts car2.rational_number
214
+ # => 3/1
215
+
216
+ ```
217
+
218
+ #### Cycles
219
+
220
+ YASL automatically restores cycles when deserializing bidirectional object references.
221
+
222
+ Example (from [samples/load_cycle.rb](samples/load_cycle.rb)):
223
+
224
+ ```ruby
225
+ require 'yasl'
226
+ require 'date'
227
+ require 'set'
228
+
229
+ class Car
230
+ attr_accessor :make,
231
+ :model,
232
+ :year,
233
+ :owner
234
+ end
235
+
236
+ class Person
237
+ class << self
238
+ def reset_count!
239
+ @count = 0
240
+ end
241
+
242
+ def increment_count!
243
+ @count ||= 0
244
+ @count += 1
245
+ end
246
+
247
+ def reset_class_count!
248
+ @@class_count = 0
249
+ end
250
+
251
+ def increment_class_count!
252
+ @@class_count = 0 unless defined?(@@class_count)
253
+ @@class_count += 1
254
+ end
255
+ end
256
+
257
+ attr_accessor :name, :dob, :cars
258
+
259
+ def initialize
260
+ self.class.increment_count!
261
+ self.class.increment_class_count!
262
+ end
263
+ end
264
+
265
+ person = Person.new
266
+ person.name = 'Sean Hux'
267
+ person.dob = Time.new(2017, 10, 17, 10, 3, 4)
268
+
269
+ car = Car.new
270
+ car.make = 'Mitsubishi'
271
+ car.model = 'Eclipse'
272
+ car.year = '2002'
273
+
274
+ car.owner = person
275
+ person.cars = [car]
276
+
277
+ dump = YASL.dump(car)
278
+ car2 = YASL.load(dump, whitelist_classes: [Car, Person])
279
+
280
+ puts car2.make
281
+ # => Mitsubishi
282
+
283
+ puts car2.model
284
+ # => Eclipse
285
+
286
+ puts car2.year
287
+ # => 2002
288
+
289
+ puts car2.owner
290
+ # => #<Person:0x00007ffdf008dc20>
291
+
292
+ puts car2.owner.name
293
+ # => Sean Hux
294
+
295
+ puts car2.owner.dob
296
+ # => 2017-10-17 10:03:04 -0400
297
+
298
+ puts car2.owner.cars.inspect
299
+ # => [#<Car:0x00007ffdf008e120 @make="Mitsubishi", @model="Eclipse", @year="2002", @owner=#<Person:0x00007ffdf008dc20 @name="Sean Hux", @dob=2017-10-17 10:03:04 -0400, @cars=[...]>>]
300
+
301
+ puts car2.inspect
302
+ # => #<Car:0x00007ffdf008e120 @make="Mitsubishi", @model="Eclipse", @year="2002", @owner=#<Person:0x00007ffdf008dc20 @name="Sean Hux", @dob=2017-10-17 10:03:04 -0400, @cars=[#<Car:0x00007ffdf008e120 ...>]>>
303
+ ```
304
+
305
+ ## Contributing
306
+
307
+ - Check out the latest master to make sure the feature hasn't been
308
+ implemented or the bug hasn't been fixed yet.
309
+ - Check out the issue tracker to make sure someone already hasn't
310
+ requested it and/or contributed it.
311
+ - Fork the project.
312
+ - Start a feature/bugfix branch.
313
+ - Commit and push until you are happy with your contribution.
314
+ - Make sure to add tests for it. This is important so I don't break it
315
+ in a future version unintentionally.
316
+ - Please try not to mess with the Rakefile, version, or history. If
317
+ you want to have your own version, or is otherwise necessary, that
318
+ is fine, but please isolate to its own commit so I can cherry-pick
319
+ around it.
320
+
321
+ ## TODO
322
+
323
+ [TODO.md](TODO.md)
324
+
325
+ ## Change Log
326
+
327
+ [CHANGELOG.md](CHANGELOG.md)
328
+
329
+ ## Copyright
330
+
331
+ [MIT](LICENSE.txt)
332
+
333
+ Copyright (c) 2020 Andy Maleh.
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,59 @@
1
+ # Copyright (c) 2020 Andy Maleh
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ require 'json'
23
+
24
+ require 'yasl/dumper'
25
+ require 'yasl/loader'
26
+
27
+ module YASL
28
+ JSON_BASIC_DATA_TYPES = ['NilClass', 'String', 'Integer', 'Float', 'TrueClass', 'FalseClass']
29
+ RUBY_ONLY_BASIC_DATA_TYPES = ['Time', 'Date', 'Complex', 'Rational', 'Regexp', 'Symbol', 'Set', 'Range', 'Array', 'Hash']
30
+ RUBY_BASIC_DATA_TYPES = RUBY_ONLY_BASIC_DATA_TYPES + JSON_BASIC_DATA_TYPES
31
+ UNSERIALIZABLE_DATA_TYPES = ['Proc', 'Binding', 'IO', 'File::Stat', 'Dir', 'BasicSocket', 'MatchData', 'Method', 'UnboundMethod', 'Thread', 'ThreadGroup', 'Continuation']
32
+
33
+ class << self
34
+ def dump(object, include_classes: false)
35
+ JSON.dump(Dumper.new(object).dump(include_classes: include_classes))
36
+ end
37
+
38
+ def load(data, include_classes: false, whitelist_classes: [])
39
+ Loader.new(JSON.load(data), whitelist_classes: whitelist_classes).load(include_classes: include_classes)
40
+ end
41
+
42
+ def json_basic_data_type?(object)
43
+ type_in?(object, JSON_BASIC_DATA_TYPES)
44
+ end
45
+
46
+ def ruby_basic_data_type?(object)
47
+ type_in?(object, RUBY_BASIC_DATA_TYPES)
48
+ end
49
+
50
+ private
51
+
52
+ def type_in?(object, types)
53
+ types.reduce(false) do |result, class_name|
54
+ result || object.class.ancestors.map(&:name).include?(class_name)
55
+ end
56
+ end
57
+
58
+ end
59
+ end
@@ -0,0 +1,193 @@
1
+ # Copyright (c) 2020 Andy Maleh
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ module YASL
23
+ class Dumper
24
+ attr_reader :object, :classes, :class_objects
25
+
26
+ def initialize(object)
27
+ @object = object
28
+ @classes = []
29
+ @class_objects = {}
30
+ end
31
+
32
+ def dump(include_classes: false)
33
+ structure = dump_structure(object)
34
+ structure.merge!(dump_classes_structure) if include_classes && structure.is_a?(Hash)
35
+ structure
36
+ end
37
+
38
+ def dump_structure(object, for_classes: false)
39
+ structure = {}
40
+ if top_level_class?(object, for_classes)
41
+ if object.name.nil?
42
+ return nil
43
+ else
44
+ structure[:_class] = object.name
45
+ add_to_classes(object)
46
+ end
47
+ elsif YASL.json_basic_data_type?(object)
48
+ structure = object
49
+ elsif YASL.ruby_basic_data_type?(object)
50
+ structure[:_class] = object.class.name
51
+ structure[:_data] = dump_ruby_basic_data_type_data(object)
52
+ else
53
+ structure.merge!(dump_non_basic_data_type_structure(object))
54
+ end
55
+ structure
56
+ end
57
+
58
+ def dump_classes_structure
59
+ structure = {}
60
+ structure[:_classes] ||= []
61
+ @original_classes = []
62
+ while classes.size > @original_classes.size
63
+ diff = (classes - @original_classes)
64
+ @original_classes = classes.clone
65
+ diff.each { |klass| structure[:_classes] << dump_class_structure(klass) }
66
+ end
67
+ structure[:_classes] = structure[:_classes].compact
68
+ structure.delete(:_classes) if structure[:_classes].empty?
69
+ structure
70
+ end
71
+
72
+ def dump_class_structure(klass)
73
+ dump_structure(klass, for_classes: true) unless klass.class_variables.empty? && klass.instance_variables.empty?
74
+ end
75
+
76
+ def dump_ruby_basic_data_type_data(object)
77
+ case object
78
+ when Time
79
+ object.to_datetime.marshal_dump
80
+ when Date
81
+ object.marshal_dump
82
+ when Complex, Rational, Regexp, Symbol
83
+ object.to_s
84
+ when Set
85
+ object.to_a.uniq.map {|element| dump_structure(element) unless unserializable?(element)}
86
+ when Range
87
+ [object.begin, object.end, object.exclude_end?]
88
+ when Array
89
+ object.map {|element| dump_structure(element) unless unserializable?(element)}
90
+ when Hash
91
+ object.reject do |key, value|
92
+ [key, value].detect {|element| unserializable?(element)}
93
+ end.map do |pair|
94
+ pair.map {|element| dump_structure(element)}
95
+ end
96
+ end
97
+ end
98
+
99
+ def dump_non_basic_data_type_structure(object)
100
+ structure = {}
101
+ klass = class_for(object)
102
+ add_to_classes(klass)
103
+ structure[:_class] = klass.name
104
+ the_class_object_id = class_object_id(object)
105
+ if the_class_object_id.nil?
106
+ structure.merge!(dump_new_non_basic_data_type_structure(object))
107
+ else
108
+ structure[:_id] = the_class_object_id
109
+ end
110
+ structure
111
+ end
112
+
113
+ def dump_new_non_basic_data_type_structure(object)
114
+ structure = {}
115
+ structure[:_id] = add_to_class_array(object) unless object.is_a?(Class) || object.is_a?(Module)
116
+ structure.merge!(dump_class_variables(object))
117
+ structure.merge!(dump_instance_variables(object))
118
+ structure.merge!(dump_struct_member_values(object))
119
+ structure
120
+ end
121
+
122
+ def dump_class_variables(object)
123
+ structure = {}
124
+ if object.respond_to?(:class_variables) && !object.class_variables.empty?
125
+ structure[:_class_variables] = object.class_variables.reduce({}) do |class_vars, var|
126
+ value = object.class_variable_get(var)
127
+ unserializable?(value) ? class_vars : class_vars.merge(var.to_s.sub('@@', '') => dump_structure(value))
128
+ end
129
+ structure.delete(:_class_variables) if structure[:_class_variables].empty?
130
+ end
131
+ structure
132
+ end
133
+
134
+ def dump_instance_variables(object)
135
+ structure = {}
136
+ if !object.instance_variables.empty?
137
+ structure[:_instance_variables] = object.instance_variables.sort.reduce({}) do |instance_vars, var|
138
+ value = object.instance_variable_get(var)
139
+ unserializable?(value) ? instance_vars : instance_vars.merge(var.to_s.sub('@', '') => dump_structure(value))
140
+ end
141
+ structure.delete(:_instance_variables) if structure[:_instance_variables].empty?
142
+ end
143
+ structure
144
+ end
145
+
146
+ def dump_struct_member_values(object)
147
+ structure = {}
148
+ if object.is_a?(Struct)
149
+ structure[:_struct_member_values] = object.members.reduce({}) do |member_values, member|
150
+ value = object[member]
151
+ value.nil? || unserializable?(value) ? member_values : member_values.merge(member => dump_structure(value))
152
+ end
153
+ structure.delete(:_struct_member_values) if structure[:_struct_member_values].empty?
154
+ end
155
+ structure
156
+ end
157
+
158
+ def unserializable?(value)
159
+ result = UNSERIALIZABLE_DATA_TYPES.detect {|class_name| value.class.ancestors.map(&:name).include?(class_name)}
160
+ result = ((value.is_a?(Class) || value.is_a?(Module)) && value.name.nil?) if result.nil?
161
+ result
162
+ end
163
+
164
+ private
165
+
166
+ def top_level_class?(object, for_classes)
167
+ (object.is_a?(Class) || object.is_a?(Module)) && !for_classes
168
+ end
169
+
170
+ def class_for(object)
171
+ object.is_a?(Class) || object.is_a?(Module) ? object : object.class
172
+ end
173
+
174
+ def add_to_classes(object)
175
+ classes << object unless classes.include?(object)
176
+ end
177
+
178
+ def class_object_id(object)
179
+ object_class_array = class_objects[class_for(object)]
180
+ object_class_array_index = object_class_array&.index(object)
181
+ (object_class_array_index + 1) unless object_class_array_index.nil?
182
+ end
183
+
184
+ def add_to_class_array(object)
185
+ object_class = class_for(object)
186
+ class_objects[object_class] ||= []
187
+ class_objects[object_class] << object unless class_objects[object_class].include?(object)
188
+ class_objects[object_class].index(object) + 1
189
+ end
190
+
191
+ end
192
+
193
+ end
@@ -0,0 +1,156 @@
1
+ # Copyright (c) 2020 Andy Maleh
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ module YASL
23
+ class Loader
24
+ attr_reader :structure, :whitelist_classes, :class_objects
25
+
26
+ def initialize(structure, whitelist_classes: [])
27
+ @structure = structure
28
+ @whitelist_classes = whitelist_classes
29
+ @class_objects = {}
30
+ end
31
+
32
+ def load(include_classes: false)
33
+ load_structure(structure).tap do
34
+ load_classes_structure if include_classes
35
+ end
36
+ end
37
+
38
+ def load_classes_structure
39
+ structure['_classes'].to_a.each do |class_structure|
40
+ load_non_basic_data_type_object(class_structure, for_classes: true)
41
+ end
42
+ end
43
+
44
+ def load_structure(structure, for_classes: false)
45
+ if top_level_class?(structure)
46
+ class_for(structure['_class'])
47
+ elsif YASL.json_basic_data_type?(structure) && !(structure.is_a?(String) && structure.start_with?('_'))
48
+ structure
49
+ elsif (structure['_class'] && (structure['_data'] || !structure.keys.detect {|key| key.start_with?('_') && key != '_class'} ))
50
+ load_ruby_basic_data_type_object(structure['_class'], structure['_data'])
51
+ elsif structure['_class'] && (structure['_id'] || structure['_instance_variables'] || structure['_class_variables'] || structure['_struct_member_values'])
52
+ load_non_basic_data_type_object(structure)
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def load_non_basic_data_type_object(structure, for_classes: false)
59
+ class_name = structure['_class']
60
+ object_class = class_for(class_name)
61
+ object_class.alias_method(:initialize_without_yasl, :initialize)
62
+ object = object_for_id(object_class, structure['_id'])
63
+ if object.nil?
64
+ object = for_classes ? object_class : object_class.new
65
+ add_to_class_array(object, structure['_id']) if !object.is_a?(Class) && !object.is_a?(Module)
66
+ end
67
+ structure['_instance_variables'].to_a.each do |instance_var, value|
68
+ value = load_structure(value)
69
+ object.instance_variable_set("@#{instance_var}".to_sym, value)
70
+ end
71
+ structure['_struct_member_values'].to_a.each do |member, value|
72
+ value = load_structure(value)
73
+ object[member.to_sym] = value
74
+ end
75
+ structure['_class_variables'].to_a.each do |class_var, value|
76
+ value = load_structure(value)
77
+ object.class_variable_set("@@#{class_var}".to_sym, value)
78
+ end
79
+ object
80
+ ensure
81
+ object_class&.define_method(:initialize, object_class.instance_method(:initialize_without_yasl))
82
+ end
83
+
84
+ def load_ruby_basic_data_type_object(class_name, data)
85
+ case class_name
86
+ when 'Time'
87
+ DateTime.new.marshal_load(data.map(&:to_r)).to_time
88
+ when 'Date'
89
+ Date.new.marshal_load(data.map(&:to_r))
90
+ when 'DateTime'
91
+ DateTime.new.marshal_load(data.map(&:to_r))
92
+ when 'Complex'
93
+ Complex(data)
94
+ when 'Rational'
95
+ Rational(data)
96
+ when 'Regexp'
97
+ Regexp.new(data)
98
+ when 'Symbol'
99
+ data.to_sym
100
+ when 'Set'
101
+ Set.new(data.map {|element| load_structure(element)})
102
+ when 'Range'
103
+ Range.new(*data)
104
+ when 'Array'
105
+ data.map {|element| load_structure(element)}
106
+ when 'Hash'
107
+ data.reduce({}) do |new_hash, pair|
108
+ new_hash.merge(load_structure(pair.first) => load_structure(pair.last))
109
+ end
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ def class_for(class_name)
116
+ class_name_components = class_name.to_s.split('::')
117
+ current_class = Object
118
+ object_class = class_name_components.reduce(Object) do |result_class, class_name|
119
+ result_class.const_get(class_name)
120
+ end
121
+ if !@whitelist_classes.include?(object_class)
122
+ raise "Class `#{class_name}` is not mentioned in `whitelist_classes` (e.g. `YASL.load(data, whitelist_classes: [#{class_name}])`)!"
123
+ end
124
+ object_class
125
+ rescue NameError
126
+ # TODO materialize a class matching the non-existing class
127
+ raise "Class `#{class_name}` does not exist! YASL expects the same classes used for serialization to exist during deserialization."
128
+ end
129
+
130
+ def top_level_class?(structure)
131
+ structure && structure.is_a?(Hash) && structure['_class'] && structure['_id'].nil? && structure['_instance_variables'].nil? && structure['_class_variables'].nil? && structure['_struct_member_values'].nil? && structure['_data'].nil?
132
+ end
133
+
134
+ def add_to_classes(object)
135
+ classes << object unless classes.include?(object)
136
+ end
137
+
138
+ def class_objects_for(object_class)
139
+ class_objects[object_class] ||= {}
140
+ end
141
+
142
+ def object_for_id(object_class, class_object_id)
143
+ return if class_object_id.nil?
144
+ return unless (object_class.is_a?(Class) || object_class.is_a?(Module))
145
+ class_objects_for(object_class)[class_object_id.to_i]
146
+ end
147
+
148
+ def add_to_class_array(object, class_object_id)
149
+ return if class_object_id.nil?
150
+ object_class = object.class
151
+ found_object = object_for_id(object_class, class_object_id)
152
+ class_objects_for(object_class)[class_object_id.to_i] = object unless found_object
153
+ end
154
+
155
+ end
156
+ end
@@ -0,0 +1,59 @@
1
+ # Generated by juwelier
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+ # stub: yasl 0.1.0 ruby lib
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "yasl".freeze
9
+ s.version = "0.1.0"
10
+
11
+ s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
+ s.require_paths = ["lib".freeze]
13
+ s.authors = ["Andy Maleh".freeze]
14
+ s.date = "2020-12-30"
15
+ s.description = "A pure Ruby serialization library that works across different Ruby implementations like Opal and JRuby as an alternative to YAML/Marshal.".freeze
16
+ s.email = "andy.am@gmail.com".freeze
17
+ s.extra_rdoc_files = [
18
+ "CHANGELOG.md",
19
+ "LICENSE.txt",
20
+ "README.md"
21
+ ]
22
+ s.files = [
23
+ "CHANGELOG.md",
24
+ "LICENSE.txt",
25
+ "README.md",
26
+ "VERSION",
27
+ "lib/yasl.rb",
28
+ "lib/yasl/dumper.rb",
29
+ "lib/yasl/loader.rb",
30
+ "yasl.gemspec"
31
+ ]
32
+ s.homepage = "http://github.com/AndyObtiva/yasl".freeze
33
+ s.licenses = ["MIT".freeze]
34
+ s.rubygems_version = "3.1.4".freeze
35
+ s.summary = "A pure Ruby serialization library that works across different Ruby implementations like Opal and JRuby as an alternative to YAML/Marshal.".freeze
36
+
37
+ if s.respond_to? :specification_version then
38
+ s.specification_version = 4
39
+ end
40
+
41
+ if s.respond_to? :add_runtime_dependency then
42
+ s.add_development_dependency(%q<rspec>.freeze, ["~> 3.5.0"])
43
+ s.add_development_dependency(%q<bundler>.freeze, [">= 1.0"])
44
+ s.add_development_dependency(%q<juwelier>.freeze, ["~> 2.1.0"])
45
+ s.add_development_dependency(%q<simplecov>.freeze, ["~> 0.16.0"])
46
+ s.add_development_dependency(%q<coveralls>.freeze, [">= 0"])
47
+ s.add_development_dependency(%q<puts_debuggerer>.freeze, [">= 0"])
48
+ s.add_development_dependency(%q<equalizer>.freeze, [">= 0"])
49
+ else
50
+ s.add_dependency(%q<rspec>.freeze, ["~> 3.5.0"])
51
+ s.add_dependency(%q<bundler>.freeze, [">= 1.0"])
52
+ s.add_dependency(%q<juwelier>.freeze, ["~> 2.1.0"])
53
+ s.add_dependency(%q<simplecov>.freeze, ["~> 0.16.0"])
54
+ s.add_dependency(%q<coveralls>.freeze, [">= 0"])
55
+ s.add_dependency(%q<puts_debuggerer>.freeze, [">= 0"])
56
+ s.add_dependency(%q<equalizer>.freeze, [">= 0"])
57
+ end
58
+ end
59
+
metadata ADDED
@@ -0,0 +1,153 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: yasl
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Andy Maleh
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-12-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 3.5.0
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 3.5.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: juwelier
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 2.1.0
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 2.1.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: simplecov
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.16.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.16.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: coveralls
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: puts_debuggerer
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: equalizer
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: A pure Ruby serialization library that works across different Ruby implementations
112
+ like Opal and JRuby as an alternative to YAML/Marshal.
113
+ email: andy.am@gmail.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files:
117
+ - CHANGELOG.md
118
+ - LICENSE.txt
119
+ - README.md
120
+ files:
121
+ - CHANGELOG.md
122
+ - LICENSE.txt
123
+ - README.md
124
+ - VERSION
125
+ - lib/yasl.rb
126
+ - lib/yasl/dumper.rb
127
+ - lib/yasl/loader.rb
128
+ - yasl.gemspec
129
+ homepage: http://github.com/AndyObtiva/yasl
130
+ licenses:
131
+ - MIT
132
+ metadata: {}
133
+ post_install_message:
134
+ rdoc_options: []
135
+ require_paths:
136
+ - lib
137
+ required_ruby_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ required_rubygems_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ requirements: []
148
+ rubygems_version: 3.1.4
149
+ signing_key:
150
+ specification_version: 4
151
+ summary: A pure Ruby serialization library that works across different Ruby implementations
152
+ like Opal and JRuby as an alternative to YAML/Marshal.
153
+ test_files: []