Project

yasl

0.0
No release in over 3 years
Low commit activity in last 3 years
A pure Ruby serialization library that works across different Ruby implementations like Opal and JRuby as an alternative to YAML/Marshal.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

>= 1.0
~> 2.1.0
~> 3.5.0
~> 0.16.0
 Project Readme

YASL - Yet Another Serialization Library

Gem Version rspec Coverage Status Maintainability

A pure Ruby auto-serialization library that works across different Ruby implementations like Opal and JRuby as an automatic alternative to YAML/Marshal. Unlike Marshal, it does not raise errors for unserializable objects, thus it always succeeds at serializing Ruby objects to provide a highly productive friction-free auto-serialization experience.

Read this blog post for an introduction.

(Note: this is an early alpha gem, so please use with caution, and report any encountered issues or feature suggestions to help improve)

Requirements

  • Portablity across different Ruby implementations, especially Opal in the web browser and JRuby.
  • Zero required configuration. Developers are too busy solving business domain problems to worry about low-level serialization details (no mucking around as_json methods and serializer classes).
  • 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

Run:

gem install yasl

Or add to Gemfile:

gem 'yasl', '~> 0.2.2'

And, run:

bundle

Finally, require in Ruby code:

require 'yasl'

Serialize

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

Keep in mind that YASL::UNSERIALIZABLE_DATA_TYPES class names are unserializable, and will serialize as nil (feel free to add more class names 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 whitelist_classes array 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 ...>]>>

Struct

Struct serialization/deserialization works out of the box in standard MRI Ruby and JRuby.

Struct in Opal has some odd JS issues and keyword_init gotchas unrelated to YASL. To completely avoid these issues, you may use the optional pure Ruby Struct re-implementation in the pure-struct gem:

require 'pure-struct' # depends on installing the [pure-struct](https://github.com/AndyObtiva/pure-struct) gem
require 'yasl'

Optionally, you may code block by the specific Ruby engine where it is needed (e.g. Opal):

if RUBY_ENGINE == 'opal'
  require 'pure-struct'
end
require 'yasl'

Contributing

  • Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
  • Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
  • Fork the project.
  • Start a feature/bugfix branch.
  • Commit and push until you are happy with your contribution.
  • Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
  • Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.

Software Process

Glimmer Process

TODO

TODO.md

Change Log

CHANGELOG.md

Copyright

MIT

Copyright (c) 2020 Andy Maleh.