yasl 0.1.0

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