Wednesday, December 30, 2020

Yet Another Serialization Library

Announcing YASL: Yet Another Serialization Library!

I know what you're thinking: "What?! Another serialization library!?! Why does it sound suspiciously like YAML? Isn't YAML enough?"

Good questions!

The short answer is YAML isn't available in Opal Ruby inside web browsers on the client-side, and Ruby Marshal raises errors by design whenever your objects reference unserializable objects like Proc.

The long answer is I needed a library that was written in Pure Ruby to ensure that it worked the same exact way in Opal, JRuby, and standard MRI Ruby so that I could use in network calls made by Glimmer applications, whether Glimmer DSL for SWT (JRuby), Glimmer DSL for Opal (Opal), or Glimmer DSL for Tk (MRI). Also, it had to silently ignore unserializable objects. Last but not least, developers are busy solving business domain problems, so the library has to require zero configuration. In other words, I don't want to fuss around with as_json methods or serializer configuration classes to manually specify attributes for JSON serialization. I just want serialization to work by passing objects in, period.

YASL took me exactly one week to write test-first till I reached the initial release. Not bad, right?

Don't get me wrong! When working strictly in standard MRI Ruby, I just use YAML. However, when building cross-Ruby apps in Glimmer that need to work in both desktop and web, YASL is the way to go!

Here is a quick intro taken straight out of the README. Enjoy!


YASL - Yet Another Serialization Library

A pure Ruby serialization library that works across different Ruby implementations like Opal and JRuby as an alternative to YAML/Marshal.

Requirements

  • Portablity across different Ruby implementations, especially Opal and JRuby.
  • Zero required configuration. Developers are too busy solving business domain problems to worry about low-level serialization details.
  • Silently ignore non-serializable objects (unlike Marshal), such as Proc, Binding, and IO.
  • No special performance requirements. No high throughput usage. Average Internet speeds.
  • Ensure system safety through secure deserialization.
  • JSON encoding is good enough. No need for premature optimization.

Usage Instructions

Serialize

To serialize, use the YASL#dump(object) method.

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):

Proc, Binding, IO, File::Stat, Dir, BasicSocket, MatchData, Method, UnboundMethod, Thread, ThreadGroup, Continuation

Example (from samples/dump_basic.rb):

require 'yasl'
require 'date'

class Car
  attr_accessor :make,
                :model,
                :year,
                :registration_time,
                :registration_date,
                :registration_date_time,
                :complex_number,
                :complex_polar_number,
                :rational_number
end

car = Car.new
car.make = 'Mitsubishi'
car.model = 'Eclipse'
car.year = '2002'
car.registration_time = Time.new(2003, 10, 19, 10, 39, 37.092, '-03:00')
car.registration_date = Date.new(2003, 10, 19)
car.registration_date_time = DateTime.new(2003, 10, 19, 10, 39, 37.092, '-03:00')
car.complex_number = Complex(2,37)
car.complex_polar_number = Complex.polar(-23,28)
car.rational_number = Rational(22/7)

dump = YASL.dump(car)

puts dump.inspect

# => "{\"_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\"}}}"
Cycles

YASL automatically detects cycles when serializing bidirectional object references.

Example (from samples/dump_cycle.rb):

require 'yasl'
require 'date'
require 'set'

class Car
  attr_accessor :make,
                :model,
                :year,
                :owner
end

class Person
  class << self
    def reset_count!
      @count = 0
    end
    
    def increment_count!
      @count ||= 0
      @count += 1
    end
    
    def reset_class_count!
      @@class_count = 0
    end
    
    def increment_class_count!
      @@class_count = 0 unless defined?(@@class_count)
      @@class_count += 1
    end
  end
  
  attr_accessor :name, :dob, :cars
  
  def initialize
    self.class.increment_count!
    self.class.increment_class_count!
  end
end

person = Person.new
person.name = 'Sean Hux'
person.dob = Time.new(2017, 10, 17, 10, 3, 4)

car = Car.new
car.make = 'Mitsubishi'
car.model = 'Eclipse'
car.year = '2002'

car.owner = person
person.cars = [car]

dump = YASL.dump(car)

puts dump.inspect

# => "{\"_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\"}}"

Deserialize

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.

By default, only YASL::RUBY_BASIC_DATA_TYPES classes are deserialized:

NilClass, String, Integer, Float, TrueClass, FalseClass, Time, Date, Complex, Rational, Regexp, Symbol, Set, Range, Array, Hash

Example (from samples/load_basic.rb):

require 'yasl'
require 'date'

class Car
  attr_accessor :make,
                :model,
                :year,
                :registration_time,
                :registration_date,
                :registration_date_time,
                :complex_number,
                :complex_polar_number,
                :rational_number
end

car = Car.new
car.make = 'Mitsubishi'
car.model = 'Eclipse'
car.year = '2002'
car.registration_time = Time.new(2003, 10, 19, 10, 39, 37.092, '-03:00')
car.registration_date = Date.new(2003, 10, 19)
car.registration_date_time = DateTime.new(2003, 10, 19, 10, 39, 37.092, '-03:00')
car.complex_number = Complex(2,37)
car.complex_polar_number = Complex.polar(-23,28)
car.rational_number = Rational(22/7)

dump = YASL.dump(car)
car2 = YASL.load(dump, whitelist_classes: [Car])

puts car2.make
# => Mitsubishi

puts car2.model
# => Eclipse

puts car2.year
# => 2002

puts car2.registration_time
# => 2003-10-19 10:39:37 -0300

puts car2.registration_date
# => 2003-10-19

puts car2.registration_date_time
# => 2003-10-19T10:39:37-03:00

puts car2.complex_number
# => 2+37i

puts car2.complex_polar_number
# => 22.13993492521203-6.230833131080988i

puts car2.rational_number
# => 3/1
Cycles

YASL automatically restores cycles when deserializing bidirectional object references.

Example (from samples/load_cycle.rb):

require 'yasl'
require 'date'
require 'set'

class Car
  attr_accessor :make,
                :model,
                :year,
                :owner
end

class Person
  class << self
    def reset_count!
      @count = 0
    end
    
    def increment_count!
      @count ||= 0
      @count += 1
    end
    
    def reset_class_count!
      @@class_count = 0
    end
    
    def increment_class_count!
      @@class_count = 0 unless defined?(@@class_count)
      @@class_count += 1
    end
  end
  
  attr_accessor :name, :dob, :cars
  
  def initialize
    self.class.increment_count!
    self.class.increment_class_count!
  end
end

person = Person.new
person.name = 'Sean Hux'
person.dob = Time.new(2017, 10, 17, 10, 3, 4)

car = Car.new
car.make = 'Mitsubishi'
car.model = 'Eclipse'
car.year = '2002'

car.owner = person
person.cars = [car]

dump = YASL.dump(car)
car2 = YASL.load(dump, whitelist_classes: [Car, Person])

puts car2.make
# => Mitsubishi

puts car2.model
# => Eclipse

puts car2.year
# => 2002

puts car2.owner
# => #<Person:0x00007ffdf008dc20>

puts car2.owner.name
# => Sean Hux

puts car2.owner.dob
# => 2017-10-17 10:03:04 -0400

puts car2.owner.cars.inspect
# => [#<Car:0x00007ffdf008e120 @make="Mitsubishi", @model="Eclipse", @year="2002", @owner=#<Person:0x00007ffdf008dc20 @name="Sean Hux", @dob=2017-10-17 10:03:04 -0400, @cars=[...]>>]

puts car2.inspect
# => #<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 ...>]>>

TODO

TODO.md

Change Log

CHANGELOG.md

Copyright

MIT

Copyright (c) 2020 Andy Maleh.

view raw yasl.md hosted with ❤ by GitHub

No comments: