arms 0.0.1.pre.rc1

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: 6b55e292c1c3dd14a37093716198e3b6f1c727f0ec5b7936cff67737abfbdab5
4
+ data.tar.gz: 34431ad4cb56af7559816402c5c9a27b68f4218728a36e5d0c077ffb99691fc4
5
+ SHA512:
6
+ metadata.gz: cc79ea29be6ced5ad4ed5e1a8a915f2993d898c44d4fa40510719ae2eeb9ab362b67194ea0ea595afa9a1cc52102ba2e098d4592d4718ba7e538d1af3d007cdd
7
+ data.tar.gz: 0e300ee399e111f75a0d1cbb144b0fe246402acfa6f64aee1c66b341c62532f28277225e890b94ca3f90c7c43fc0bc1af9010eb393f6d6385e272683f4171469
@@ -0,0 +1 @@
1
+ SimpleCov.start
@@ -0,0 +1 @@
1
+ --main README.md --markup=markdown {lib}/**/*.rb
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 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.
@@ -0,0 +1,82 @@
1
+ # ARMS
2
+
3
+ [![Build Status](https://travis-ci.org/notEthan/arms.svg?branch=master)](https://travis-ci.org/notEthan/arms)
4
+ [![Coverage Status](https://coveralls.io/repos/github/notEthan/arms/badge.svg)](https://coveralls.io/github/notEthan/arms)
5
+
6
+ ARMS: Active Record Multiple Serialization. This is a library which extends the capabilities of ActiveRecord serialization, allowing you to chain together coders.
7
+
8
+ For a very simple example, we'll do a thing you can't easily do in ActiveRecord: get a hash with indifferent string/symbol access on the model (in this case for a column named `preferences`), storing serialized JSON in the database:
9
+
10
+ ```ruby
11
+ require 'arms'
12
+ class Foo < ActiveRecord::Base
13
+ arms_serialize :preferences, :indifferent_hashes, JSON
14
+ end
15
+ ```
16
+
17
+ assuming you have a database set up with table `foos` and string column `preferences` (see [this script](https://gist.github.com/notEthan/84a4e583ea6e96f0f92dab43286ba301) for a full example), the database will contain JSON (seen with `#preferences_before_type_cast`) and the model attribute offers indifferent access.
18
+
19
+ ```ruby
20
+ Foo.create!(preferences: {favorite_animal: 'ocelot'})
21
+
22
+ foo = foo.last
23
+
24
+ # JSON in the DB:
25
+ foo.preferences_before_type_cast
26
+ # => "{\"favorite_animal\":\"ocelot\"}"
27
+
28
+ # indifferent access on the model:
29
+ foo.preferences[:favorite_animal]
30
+ # => 'ocelot'
31
+ foo.preferences['favorite_animal']
32
+ # => 'ocelot'
33
+ ```
34
+
35
+ ## Coder Shortcuts
36
+
37
+ With stock ActiveRecord, you can call `serialize :foo, JSON` which is a sort of shortcut to the coder `ActiveRecord::Coders::JSON`. ARMS extends this a bit and offers a registry of shortcuts. In the above example, `:indifferent_hashes` is a shortcut invoking the coder `ARMS::IndifferentHashesCoder`. Most shortcut keys are symbols, but sometimes classes such as JSON or YAML are shortcut keys to coders for those serializations.
38
+
39
+ Some coders take arguments when they are instantiated. Shortcuts can be expressed as an array, where the first element is the shortcut key and the remainder of the array is passed as arguments to instantiate the coder - for example, the YAML coder can take an argument hinting what class it expects to be serializing. Modifying the above, this would look like:
40
+
41
+ ```ruby
42
+ class Foo < ActiveRecord::Base
43
+ arms_serialize :preferences, :indifferent_hashes, [YAML, Hash]
44
+ end
45
+ ```
46
+
47
+ A full example with this serialization is [at this link](https://gist.github.com/notEthan/297243912fcbd07354fc3d48093df12f).
48
+
49
+ ### Built-in Shortcuts
50
+
51
+ The following shortcuts are built into ARMS:
52
+
53
+ | Shortcut Key | Loads | Dumps | Arguments | Coder Class |
54
+ | --- | --- | --- | --- | --- |
55
+ | JSON | A string of JSON | Ruby Arrays, Hashes, and basic types | none | ActiveRecord::Coders::JSON |
56
+ | :json | ^ | ^ | ^ | ^ |
57
+ | YAML | A string of YAML | Any | Expected loaded class | ActiveRecord::Coders::YAMLColumn |
58
+ | :yaml | ^ | ^ | ^ | ^ |
59
+ | :indifferent_hashes | Indifferentiated structure of Arrays and Hashes | Plain structure of Arrays and Hashes | none | ARMS::IndifferentHashesCoder |
60
+ | :struct | An instance or array of instances of a Struct class | A Hash or Array of Hashes | The Struct class to instantiate | ARMS::StructCoder |
61
+
62
+ ## Provided Coders
63
+
64
+ ARMS offers a few useful coders which may be used with arms_serialize, or with vanilla ActiveRecord::Base.serialize. For the most part these aim to have JSONifiable data on the #dump side, which may be stored in a JSON column or serialized to text with yaml or json.
65
+
66
+ ### ARMS::IndifferentHashesCoder
67
+
68
+ When loading, this coder takes a JSONifiable structure of arrays and hashes, and will change Hash instances to ActiveSupport::HashWithIndifferentAccess.
69
+
70
+ When dumping, it converts indifferent hashes to plain hashes.
71
+
72
+ ### ARMS::StructCoder
73
+
74
+ Instantiated with a Struct class, this converts an instance or instances of that struct to JSONifiable types.
75
+
76
+ When loading a hash (or array of hashes), the given struct class is instatiated with each member corresponding to a key of a hash.
77
+
78
+ When dumping, an instance of the specified struct class (or array of instances) is dumped to a hash in which each member of the struct and its value is a key/value pair.
79
+
80
+ ## License
81
+
82
+ ARMS is open source software available under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -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
@@ -0,0 +1,37 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "arms/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "arms"
7
+ spec.version = ARMS::VERSION
8
+ spec.authors = ["Ethan"]
9
+ spec.email = ["ethan@unth.net"]
10
+
11
+ spec.summary = 'Active Record Multiple Serialization'
12
+ spec.description = 'A library which offers flexible, chained serializion for Active Record'
13
+ spec.homepage = "https://github.com/notEthan/arms"
14
+ spec.license = "MIT"
15
+
16
+ ignore_files = %w(.gitignore .travis.yml Gemfile test)
17
+ ignore_files_re = %r{\A(#{ignore_files.map { |f| Regexp.escape(f) }.join('|')})(/|\z)}
18
+ # Specify which files should be added to the gem when it is released.
19
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
20
+ Dir.chdir(File.expand_path('..', __FILE__)) do
21
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(ignore_files_re) }
22
+ spec.test_files = `git ls-files -z test`.split("\x0") + [
23
+ '.simplecov',
24
+ ]
25
+ end
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_dependency "activerecord"
31
+ spec.add_development_dependency "bundler", "~> 2.0"
32
+ spec.add_development_dependency "rake", "~> 10.0"
33
+ spec.add_development_dependency "minitest", "~> 5.0"
34
+ spec.add_development_dependency "minitest-reporters"
35
+ spec.add_development_dependency "simplecov"
36
+ spec.add_development_dependency "sqlite3", "~> 1.3", ">= 1.3.6" # loosen this in accordance with active_record/connection_adapters/sqlite3_adapter.rb
37
+ end
@@ -0,0 +1,96 @@
1
+ require "arms/version"
2
+
3
+ module ARMS
4
+ # base class for ARMS errors
5
+ class Error < StandardError
6
+ end
7
+
8
+ # a coder which is not a recognized shortcut and/or does not respond to #load and #dump
9
+ class InvalidCoder < Error
10
+ attr_accessor :coder
11
+ end
12
+
13
+ # an error loading column data to objects on the model
14
+ class LoadError < Error
15
+ end
16
+
17
+ # an error dumping objects from the model to column data
18
+ class DumpError < Error
19
+ end
20
+
21
+ # the object passed to a coder shortcut proc, which indicates the model and attribute name
22
+ # and passes optional arguments used to instantiate the coder.
23
+ class ShortcutInvocation
24
+ # the model on which an attribute is being serialized
25
+ attr_accessor :model
26
+ # the name of the attribute being serialized
27
+ attr_accessor :attr_name
28
+ # arguments passed from the shortcut invocation to the coder shortcut proc
29
+ attr_accessor :args
30
+ end
31
+
32
+ @coder_shortcuts = {}
33
+
34
+ class << self
35
+ # adds a shortcut which can be used with ActiveRecord::Base.arms_serialize. the key is usually
36
+ # a symbol, but may be anything. the given block is called by arms_serialize with an
37
+ # ARMS::ShortcutInvocation object, and must result in a coder.
38
+ #
39
+ # @yieldparam shortcut_invocation [ARMS::ShortcutInvocation]
40
+ # @yieldreturn [#load, #dump] a coder which responds to #load and #dump
41
+ def register_coder_shortcut(key, &coderproc)
42
+ raise(ArgumentError, "already registered shortcut: #{key}") if @coder_shortcuts.key?(key)
43
+ @coder_shortcuts[key] = coderproc
44
+ nil
45
+ end
46
+ end
47
+
48
+ autoload :MultiCoder, 'arms/multi_coder'
49
+ autoload :IndifferentHashesCoder, 'arms/indifferent_hashes_coder'
50
+ autoload :StructCoder, 'arms/struct_coder'
51
+ end
52
+
53
+ require 'json'
54
+ ARMS.register_coder_shortcut(JSON) { ::ActiveRecord::Coders::JSON }
55
+ ARMS.register_coder_shortcut(:json) { ::ActiveRecord::Coders::JSON }
56
+
57
+ require 'yaml'
58
+ ARMS.register_coder_shortcut(YAML) { |s| ::ActiveRecord::Coders::YAMLColumn.new(s.attr_name, *s.args) }
59
+ ARMS.register_coder_shortcut(:yaml) { |s| ::ActiveRecord::Coders::YAMLColumn.new(s.attr_name, *s.args) }
60
+
61
+ ARMS.register_coder_shortcut(:indifferent_hashes) { ARMS::IndifferentHashesCoder.new }
62
+ ARMS.register_coder_shortcut(:struct) { |s| ARMS::StructCoder.new(*s.args) }
63
+
64
+ module ARMS
65
+ module ActiveRecord
66
+ module AttributeMethods
67
+ module Serialization
68
+ # ActiveRecord::Base.arms_serialize takes an attribute name and any number of coders which
69
+ # will be chained to serialize and deserialize between the database column and the model
70
+ # attribute.
71
+ #
72
+ # full documentation is at {ARMS::MultiCoder#initialize}.
73
+ #
74
+ # here are a few example invocations:
75
+ #
76
+ # # two coders: indifferent hashes, YAML with argument Hash (the object_class)
77
+ # arms_serialize('preferences', :indifferent_hashes, [YAML, Hash])
78
+ #
79
+ # # two coders: struct coder with argument Preference (the struct class), JSON coder
80
+ # MultiCoder.new([[:struct, Preference], :json], attr_name: 'preferences', model: Foo)
81
+ def arms_serialize(attr_name, *coders)
82
+ multi_coder = ARMS::MultiCoder.new(coders, attr_name: attr_name, model: self)
83
+ serialize(attr_name, multi_coder)
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ require 'active_record'
91
+
92
+ module ActiveRecord
93
+ class Base
94
+ extend ARMS::ActiveRecord::AttributeMethods::Serialization
95
+ end
96
+ end
@@ -0,0 +1,26 @@
1
+ module ARMS
2
+ # ARMS::IndifferentHashesCoder will replace any Hashes in a structure of Arrays and Hashes with ActiveSupport::HashWithIndifferentAccess on load, and convert back to plain hashes on dump.
3
+ class IndifferentHashesCoder
4
+ # @param column_data [Array, Hash, Object] a structure in which Hashes will be replaced with ActiveSupport::HashWithIndifferentAccess
5
+ def load(column_data)
6
+ if column_data.respond_to?(:to_ary)
7
+ column_data.to_ary.map { |el| load(el) }
8
+ elsif column_data.respond_to?(:to_hash)
9
+ ActiveSupport::HashWithIndifferentAccess.new(column_data).transform_values { |v| load(v) }
10
+ else
11
+ column_data
12
+ end
13
+ end
14
+
15
+ # @param object [#to_ary, #to_hash, Object] a structure in which ActiveSupport::HashWithIndifferentAccess instances will be replaced with plain Hashes
16
+ def dump(object)
17
+ if object.respond_to?(:to_ary)
18
+ object.to_ary.map { |el| dump(el) }
19
+ elsif object.respond_to?(:to_hash)
20
+ object.to_hash.transform_values { |v| dump(v) }
21
+ else
22
+ object
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,84 @@
1
+ module ARMS
2
+ class MultiCoder
3
+ # loads and dumps between database column and model attribute, using any number of coders.
4
+ #
5
+ # the first coder is closest to the loaded model attribute. the last coder is closest to
6
+ # the dumped database column.
7
+ #
8
+ # each coder must respond to #load and #dump. such a coder can be passed directly, or as a
9
+ # shortcut consisting of a key registered with ARMS.register_coder_shortcut and optional
10
+ # arguments (using an array). each of the following is a valid coder (an element of the
11
+ # coders array):
12
+ #
13
+ # # direct reference to the coder
14
+ # ::ActiveRecord::Coders::YAMLColumn.new('foo')
15
+ #
16
+ # # shortcut, equivalent to the above
17
+ # :yaml
18
+ #
19
+ # # shortcut passing optional `object_class` argument to yaml coder.
20
+ # # the first element of this array is the shortcut key, and the remainder
21
+ # # is arguments passed to instantiate the coder.
22
+ # [:yaml, Array]
23
+ #
24
+ # here are a few example invocations that instantiate a MultiCoder:
25
+ #
26
+ # # two coders: indifferent hashes, YAML with argument Hash (the object_class)
27
+ # MultiCoder.new([:indifferent_hashes, [YAML, Hash]], attr_name: 'preferences', model: Foo)
28
+ #
29
+ # # two coders: struct coder with argument Preference (the struct class), JSON coder
30
+ # MultiCoder.new([[:struct, Preference], :json], attr_name: 'preferences', model: Foo)
31
+ #
32
+ # load goes like:
33
+ #
34
+ # database column -> coderN.load -> ... -> coder1.load -> model attribute
35
+ #
36
+ # dump goes like:
37
+ #
38
+ # model attribute -> coder1.dump -> ... -> coderN.dump -> database column
39
+ #
40
+ # @param coders [Array] an array of coders (which respond to #load and #dump) or coder shortcuts
41
+ # @param model [Class] the model on which the attribute is being serialized
42
+ # @param attr_name the attribute name being serialized on the model
43
+ def initialize(coders, model: nil, attr_name: nil)
44
+ @coders = coders.each_with_index.map do |coder, i|
45
+ shortcut_invocation = ShortcutInvocation.new
46
+ shortcut_invocation.model = model
47
+ shortcut_invocation.attr_name = attr_name
48
+
49
+ if coder.respond_to?(:to_ary)
50
+ shortcut_invocation.args = coder[1..-1]
51
+ coder = coder[0]
52
+ end
53
+
54
+ if ARMS.instance_exec { @coder_shortcuts }.key?(coder)
55
+ ARMS.instance_exec { @coder_shortcuts }[coder].(shortcut_invocation)
56
+ elsif coder.respond_to?(:load) && coder.respond_to?(:dump)
57
+ if shortcut_invocation.args.nil? || shortcut_invocation.args.empty?
58
+ coder
59
+ else
60
+ raise(InvalidCoder.new("given shortcut arguments are not passed to the coder at index #{i} which responds to #load and #dump. coder: #{coder.inspect}; shortcut args: #{shortcut_invocation.args.inspect}").tap { |e| e.coder = coder })
61
+ end
62
+ else
63
+ raise(InvalidCoder.new("given coder at index #{i} is not a recognized shortcut and does not respond to #load and #dump. coder: #{coder.inspect}; shortcut args: #{shortcut_invocation.args.inspect}").tap { |e| e.coder = coder })
64
+ end
65
+ end
66
+ end
67
+
68
+ # @param column_data [Object] data hot off the database column
69
+ # @return [Object] loaded (deserialized) data
70
+ def load(column_data)
71
+ @coders.reverse.inject(column_data) do |data, coder|
72
+ coder.load(data)
73
+ end
74
+ end
75
+
76
+ # @param object [Object] object on the model attribute
77
+ # @return [Object] dumped (serialized) data
78
+ def dump(object)
79
+ @coders.inject(object) do |data, coder|
80
+ coder.dump(data)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,93 @@
1
+ module ARMS
2
+ # this is a ActiveRecord serialization class intended to serialize from a Struct class
3
+ # on the loaded ruby side to something JSON-compatible on the dumped database side.
4
+ #
5
+ # This coder relies on `loaded_class`, the Struct class which will be used to instantiate
6
+ # the column data. properties (members) of the loaded class will correspond
7
+ # to keys of the dumped json object.
8
+ #
9
+ # the data may be either a single instance of the loaded class
10
+ # (serialized as one hash) or an array of them (serialized as an
11
+ # array of hashes), indicated by the boolean keyword argument `array`.
12
+ #
13
+ # the column behind the attribute may be an actual JSON column (postgres json
14
+ # or jsonb - hstore should work too if you only have string attributes) or may
15
+ # be a string column with a string serializer after StructCoder.
16
+ class StructCoder
17
+ # @param loaded_class [Class] the Struct class to load
18
+ # @param array [Boolean] whether the column holds an array of Struct instances instead of just one
19
+ def initialize(loaded_class, array: false)
20
+ @loaded_class = loaded_class
21
+ # this notes the order of the keys as they were in the json, used by dump_object to generate
22
+ # json that is equivalent to the json/jsonifiable that came in, so that AR's #changed_attributes
23
+ # can tell whether the attribute has been changed.
24
+ @loaded_class.send(:attr_accessor, :arms_object_json_coder_keys_order)
25
+ @array = array
26
+ end
27
+
28
+ # @param data [Hash, Array<Hash>]
29
+ # @return [loaded_class, Array[loaded_class]]
30
+ def load(data)
31
+ return nil if data.nil?
32
+ object = if @array
33
+ unless data.respond_to?(:to_ary)
34
+ raise LoadError, "expected array-like column data; got: #{data.class}: #{data.inspect}"
35
+ end
36
+ data.map { |el| load_object(el) }
37
+ else
38
+ load_object(data)
39
+ end
40
+ object
41
+ end
42
+
43
+ # @param object [loaded_class, Array[loaded_class]]
44
+ # @return [Hash, Array<Hash>]
45
+ def dump(object)
46
+ return nil if object.nil?
47
+ jsonifiable = begin
48
+ if @array
49
+ unless object.respond_to?(:to_ary)
50
+ raise DumpError, "expected array-like attribute; got: #{object.class}: #{object.inspect}"
51
+ end
52
+ object.map do |el|
53
+ dump_object(el)
54
+ end
55
+ else
56
+ dump_object(object)
57
+ end
58
+ end
59
+ jsonifiable
60
+ end
61
+
62
+ private
63
+
64
+ # @param data [Hash]
65
+ # @return [loaded_class]
66
+ def load_object(data)
67
+ if data.respond_to?(:to_hash)
68
+ data = data.to_hash
69
+ good_keys = @loaded_class.members.map(&:to_s)
70
+ bad_keys = data.keys - good_keys
71
+ unless bad_keys.empty?
72
+ raise LoadError, "expected keys #{good_keys}; got unrecognized keys: #{bad_keys}"
73
+ end
74
+ instance = @loaded_class.new(*@loaded_class.members.map { |m| data[m.to_s] })
75
+ instance.arms_object_json_coder_keys_order = data.keys
76
+ instance
77
+ else
78
+ raise LoadError, "expected instance(s) of #{Hash}; got: #{data.class}: #{data.inspect}"
79
+ end
80
+ end
81
+
82
+ # @param object [loaded_class]
83
+ # @return [Hash]
84
+ def dump_object(object)
85
+ if object.is_a?(@loaded_class)
86
+ keys = (object.arms_object_json_coder_keys_order || []) | @loaded_class.members.map(&:to_s)
87
+ keys.map { |member| {member => object[member]} }.inject({}, &:update)
88
+ else
89
+ raise TypeError, "expected instance(s) of #{@loaded_class}; got: #{object.class}: #{object.inspect}"
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,3 @@
1
+ module ARMS
2
+ VERSION = "0.0.1-rc1"
3
+ end
@@ -0,0 +1,140 @@
1
+ require "test_helper"
2
+
3
+ class ARMSTest < Minitest::Test
4
+ def test_that_it_has_a_version_number
5
+ refute_nil ::ARMS::VERSION
6
+ end
7
+ end
8
+
9
+ describe 'ActiveRecord::Base.arms_serialize' do
10
+ describe 'serializing to JSON' do
11
+ it 'serializes json with JSON' do
12
+ Blog::Foo.create!(tags_const_json: {'#BlackLivesMatter' => {rank: 1}})
13
+ assert_equal(%q({"#BlackLivesMatter":{"rank":1}}), Blog::UnserializedFoo.last.tags_const_json)
14
+ end
15
+ it 'serializes json with :json' do
16
+ Blog::Foo.create!(tags_sym_json: {'#BlackLivesMatter' => {rank: 1}})
17
+ assert_equal(%q({"#BlackLivesMatter":{"rank":1}}), Blog::UnserializedFoo.last.tags_sym_json)
18
+ end
19
+ end
20
+ describe 'serializing to YAML' do
21
+ it 'serializes yaml with YAML' do
22
+ Blog::Foo.create!(tags_const_yaml: {'#BlackLivesMatter' => {rank: 1}})
23
+ assert_equal(%Q(---\n"#BlackLivesMatter":\n :rank: 1\n), Blog::UnserializedFoo.last.tags_const_yaml)
24
+ end
25
+ it 'serializes yaml with :yaml' do
26
+ Blog::Foo.create!(tags_sym_yaml: {'#BlackLivesMatter' => {rank: 1}})
27
+ assert_equal(%Q(---\n"#BlackLivesMatter":\n :rank: 1\n), Blog::UnserializedFoo.last.tags_sym_yaml)
28
+ end
29
+ end
30
+ describe 'deserializing with indifferent access' do
31
+ it 'deserializes yaml with string keys with indifferent access shortcut' do
32
+ Blog::UnserializedFoo.create!(tags_indifferent_yaml: %Q(---\n"#BlackLivesMatter":\n rank: 1\n))
33
+ assert_equal({'#BlackLivesMatter' => {'rank' => 1}}, Blog::Foo.last.tags_indifferent_yaml)
34
+ assert_instance_of(ActiveSupport::HashWithIndifferentAccess, Blog::Foo.last.tags_indifferent_yaml)
35
+ end
36
+ it 'deserializes yaml with symbol keys with indifferent access shortcut' do
37
+ Blog::UnserializedFoo.create!(tags_indifferent_yaml: %Q(---\n"#BlackLivesMatter":\n :rank: 1\n))
38
+ assert_equal({'#BlackLivesMatter' => {'rank' => 1}}, Blog::Foo.last.tags_indifferent_yaml)
39
+ assert_instance_of(ActiveSupport::HashWithIndifferentAccess, Blog::Foo.last.tags_indifferent_yaml)
40
+ end
41
+ it 'deserializes json with indifferent access shortcut' do
42
+ Blog::UnserializedFoo.create!(tags_indifferent_json: %q({"#BlackLivesMatter":{"rank":1}}))
43
+ assert_equal({'#BlackLivesMatter' => {'rank' => 1}}, Blog::Foo.last.tags_indifferent_json)
44
+ assert_instance_of(ActiveSupport::HashWithIndifferentAccess, Blog::Foo.last.tags_indifferent_json)
45
+ end
46
+ it 'deserializes json with ARMS::IndifferentHashesCoder' do
47
+ Blog::UnserializedFoo.create!(tags_const_indifferent_json: %q({"#BlackLivesMatter":{"rank":1,"x":[{"y":"z"}]}}))
48
+ assert_equal({'#BlackLivesMatter' => {'rank' => 1, 'x' => [{'y' => 'z'}]}}, Blog::Foo.last.tags_const_indifferent_json)
49
+ assert_instance_of(ActiveSupport::HashWithIndifferentAccess, Blog::Foo.last.tags_const_indifferent_json)
50
+ assert_instance_of(ActiveSupport::HashWithIndifferentAccess, Blog::Foo.last.tags_const_indifferent_json['#BlackLivesMatter']['x'][0])
51
+ end
52
+ end
53
+ describe 'serializing with indifferent access' do
54
+ it 'serializes yaml with string keys with indifferent access shortcut' do
55
+ Blog::Foo.create!(tags_indifferent_yaml: {'#BlackLivesMatter' => {'rank' => 1}})
56
+ assert_equal(%Q(---\n"#BlackLivesMatter":\n rank: 1\n), Blog::UnserializedFoo.last.tags_indifferent_yaml)
57
+ end
58
+ it 'serializes yaml with symbol keys with indifferent access shortcut' do
59
+ Blog::Foo.create!(tags_indifferent_yaml: {'#BlackLivesMatter' => {rank: 1}})
60
+ assert_equal(%Q(---\n"#BlackLivesMatter":\n rank: 1\n), Blog::UnserializedFoo.last.tags_indifferent_yaml)
61
+ end
62
+ it 'serializes json with indifferent access shortcut' do
63
+ Blog::Foo.create!(tags_indifferent_json: {'#BlackLivesMatter' => {'rank' => 1}})
64
+ assert_equal(%q({"#BlackLivesMatter":{"rank":1}}), Blog::UnserializedFoo.last.tags_indifferent_json)
65
+ end
66
+ it 'serializes json with ARMS::IndifferentHashesCoder' do
67
+ Blog::Foo.create!(tags_const_indifferent_json: {'#BlackLivesMatter' => {rank: 1, x: [{y: 'z'}]}})
68
+ assert_equal(%q({"#BlackLivesMatter":{"rank":1,"x":[{"y":"z"}]}}), Blog::UnserializedFoo.last.tags_const_indifferent_json)
69
+ end
70
+ end
71
+ describe 'deserializing to structs' do
72
+ it 'deserializes json array of tags to structs' do
73
+ Blog::UnserializedFoo.create!(tags_ary_struct_json: %q([{"name":"#BlackLivesMatter","rank":1}]))
74
+ tag = Blog::Foo.last.tags_ary_struct_json.last
75
+ assert_equal("#BlackLivesMatter", tag.name)
76
+ assert_equal(1, tag.rank)
77
+ assert_instance_of(Blog::Tag, tag)
78
+ end
79
+ it 'deserializes yaml array of tags to structs' do
80
+ Blog::UnserializedFoo.create!(tags_ary_struct_yaml: %Q(---\n- name: "#BlackLivesMatter"\n rank: 1\n))
81
+ tag = Blog::Foo.last.tags_ary_struct_yaml.last
82
+ assert_equal("#BlackLivesMatter", tag.name)
83
+ assert_equal(1, tag.rank)
84
+ assert_instance_of(Blog::Tag, tag)
85
+ end
86
+ it 'deserializes json array of tags to structs (tags_ary_struct_inst_json)' do
87
+ Blog::UnserializedFoo.create!(tags_ary_struct_inst_json: %q([{"name":"#BlackLivesMatter","rank":1}]))
88
+ tag = Blog::Foo.last.tags_ary_struct_inst_json.last
89
+ assert_equal("#BlackLivesMatter", tag.name)
90
+ assert_equal(1, tag.rank)
91
+ assert_instance_of(Blog::Tag, tag)
92
+ end
93
+ it 'deserializes yaml array of tags to structs (tags_ary_struct_inst_yaml)' do
94
+ Blog::UnserializedFoo.create!(tags_ary_struct_inst_yaml: %Q(---\n- name: "#BlackLivesMatter"\n rank: 1\n))
95
+ tag = Blog::Foo.last.tags_ary_struct_inst_yaml.last
96
+ assert_equal("#BlackLivesMatter", tag.name)
97
+ assert_equal(1, tag.rank)
98
+ assert_instance_of(Blog::Tag, tag)
99
+ end
100
+ it 'newest_tag' do
101
+ Blog::UnserializedFoo.create!(newest_tag: %Q({"name":"#arms","rank":5280}))
102
+ foo = Blog::Foo.last
103
+ assert_equal("#arms", foo.newest_tag.name)
104
+ assert_equal(5280, foo.newest_tag.rank)
105
+ assert_instance_of(Blog::Tag, foo.newest_tag)
106
+ end
107
+ end
108
+ describe 'serializing to structs' do
109
+ it 'deserializes json array of tags to structs' do
110
+ Blog::Foo.create!(tags_ary_struct_json: [Blog::Tag.new('#BlackLivesMatter', 1)])
111
+ assert_equal(%q([{"name":"#BlackLivesMatter","rank":1}]), Blog::UnserializedFoo.last.tags_ary_struct_json)
112
+ end
113
+ it 'deserializes yaml array of tags to structs' do
114
+ Blog::Foo.create!(tags_ary_struct_yaml: [Blog::Tag.new('#BlackLivesMatter', 1)])
115
+ assert_equal(%Q(---\n- name: "#BlackLivesMatter"\n rank: 1\n), Blog::UnserializedFoo.last.tags_ary_struct_yaml)
116
+ end
117
+ it 'deserializes json array of tags to structs (tags_ary_struct_inst_json)' do
118
+ Blog::Foo.create!(tags_ary_struct_inst_json: [Blog::Tag.new('#BlackLivesMatter', 1)])
119
+ assert_equal(%q([{"name":"#BlackLivesMatter","rank":1}]), Blog::UnserializedFoo.last.tags_ary_struct_inst_json)
120
+ end
121
+ it 'deserializes yaml array of tags to structs (tags_ary_struct_inst_yaml)' do
122
+ Blog::Foo.create!(tags_ary_struct_inst_yaml: [Blog::Tag.new('#BlackLivesMatter', 1)])
123
+ assert_equal(%Q(---\n- name: "#BlackLivesMatter"\n rank: 1\n), Blog::UnserializedFoo.last.tags_ary_struct_inst_yaml)
124
+ end
125
+ it 'newest_tag' do
126
+ Blog::Foo.create!(newest_tag: Blog::Tag.new('#arms', 5280))
127
+ assert_equal(%Q({"name":"#arms","rank":5280}), Blog::UnserializedFoo.last.newest_tag)
128
+ end
129
+ end
130
+ describe 'incorrect invocation' do
131
+ it 'raises when trying to pass arguments to a coder that is not a shortcut' do
132
+ err = assert_raises(ARMS::InvalidCoder) { Blog::Foo.arms_serialize('x', [ARMS::IndifferentHashesCoder.new, 3]) }
133
+ assert_match(%r(given shortcut arguments are not passed to the coder at index 0 which responds to #load and #dump\. coder: #<ARMS::IndifferentHashesCoder.*>; shortcut args: \[3\]), err.message)
134
+ end
135
+ it 'raises when given a coder that is not a coder' do
136
+ err = assert_raises(ARMS::InvalidCoder) { Blog::Foo.arms_serialize('x', "jsonify this pls") }
137
+ assert_equal("given coder at index 0 is not a recognized shortcut and does not respond to #load and #dump. coder: \"jsonify this pls\"; shortcut args: nil", err.message)
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,56 @@
1
+ require 'logger'
2
+ module Blog
3
+ logpath = Pathname.new('log/test.log')
4
+ FileUtils.mkdir_p(logpath.dirname)
5
+ -> (logger) { define_singleton_method(:logger) { logger } }.(::Logger.new(logpath))
6
+ logger.level = ::Logger::INFO
7
+ end
8
+
9
+ require 'active_record'
10
+ ActiveRecord::Base.logger = Blog.logger
11
+ dbpath = Pathname.new('tmp/blog.sqlite3')
12
+ FileUtils.mkdir_p(dbpath.dirname)
13
+ dbpath.unlink if dbpath.exist?
14
+ ActiveRecord::Base.establish_connection({
15
+ :adapter => "sqlite3",
16
+ :database => dbpath,
17
+ })
18
+
19
+ ActiveRecord::Schema.define do
20
+ create_table :foos do |table|
21
+ table.column :tags_const_json, :string
22
+ table.column :tags_const_yaml, :string
23
+ table.column :tags_sym_json, :string
24
+ table.column :tags_sym_yaml, :string
25
+ table.column :tags_indifferent_json, :string
26
+ table.column :tags_indifferent_yaml, :string
27
+ table.column :tags_const_indifferent_json, :string
28
+ table.column :tags_ary_struct_json, :string
29
+ table.column :tags_ary_struct_yaml, :string
30
+ table.column :tags_ary_struct_inst_json, :string
31
+ table.column :tags_ary_struct_inst_yaml, :string
32
+ table.column :newest_tag, :string
33
+ end
34
+ end
35
+
36
+ module Blog
37
+ Tag = Struct.new(:name, :rank)
38
+
39
+ class Foo < ActiveRecord::Base
40
+ arms_serialize :tags_const_json, JSON
41
+ arms_serialize :tags_const_yaml, YAML
42
+ arms_serialize :tags_sym_json, :json
43
+ arms_serialize :tags_sym_yaml, :yaml
44
+ arms_serialize :tags_indifferent_json, :indifferent_hashes, :json
45
+ arms_serialize :tags_indifferent_yaml, :indifferent_hashes, :yaml
46
+ arms_serialize :tags_const_indifferent_json, ARMS::IndifferentHashesCoder.new, JSON
47
+ arms_serialize :tags_ary_struct_json, [:struct, Blog::Tag, array: true], :json
48
+ arms_serialize :tags_ary_struct_yaml, [:struct, Blog::Tag, array: true], :yaml
49
+ arms_serialize :tags_ary_struct_inst_json, ARMS::StructCoder.new(Blog::Tag, array: true), :json
50
+ arms_serialize :tags_ary_struct_inst_yaml, ARMS::StructCoder.new(Blog::Tag, array: true), :yaml
51
+ arms_serialize :newest_tag, [:struct, Blog::Tag], :json
52
+ end
53
+ class UnserializedFoo < ActiveRecord::Base
54
+ self.table_name = 'foos'
55
+ end
56
+ end
@@ -0,0 +1,69 @@
1
+ require_relative 'test_helper'
2
+
3
+ describe ARMS::StructCoder do
4
+ let(:struct) { Struct.new(:foo, :bar) }
5
+ let(:options) { {} }
6
+ let(:struct_coder) { ARMS::StructCoder.new(struct, options) }
7
+ describe 'json' do
8
+ describe 'load' do
9
+ it 'loads nil' do
10
+ assert_nil(struct_coder.load(nil))
11
+ end
12
+ it 'loads a hash' do
13
+ assert_equal(struct.new('bar'), struct_coder.load({"foo" => "bar"}))
14
+ end
15
+ it 'loads something else' do
16
+ assert_raises(ARMS::LoadError) do
17
+ struct_coder.load([[]])
18
+ end
19
+ end
20
+ it 'loads unrecognized keys' do
21
+ assert_raises(ARMS::LoadError) do
22
+ struct_coder.load({"uhoh" => "spaghettio"})
23
+ end
24
+ end
25
+ describe 'array' do
26
+ let(:options) { {array: true} }
27
+ it 'loads an array of hashes' do
28
+ data = [{"foo" => "bar"}, {"foo" => "baz"}]
29
+ assert_equal([struct.new('bar'), struct.new('baz')], struct_coder.load(data))
30
+ end
31
+ it 'loads an empty array' do
32
+ assert_equal([], struct_coder.load([]))
33
+ end
34
+ it 'does not load what is not an array of structs' do
35
+ assert_raises(ARMS::LoadError) { struct_coder.load({"foo" => "bar"}) }
36
+ end
37
+ end
38
+ end
39
+ describe 'dump' do
40
+ it 'dumps nil' do
41
+ assert_nil(struct_coder.dump(nil))
42
+ end
43
+ it 'dumps a struct' do
44
+ assert_equal({"foo" => "x", "bar" => "y"}, struct_coder.dump(struct.new('x', 'y')))
45
+ end
46
+ it 'dumps something else' do
47
+ assert_raises(TypeError) do
48
+ struct_coder.dump(Object.new)
49
+ end
50
+ end
51
+ it 'dumps all the keys of a struct after loading in a partial one' do
52
+ struct = struct_coder.load({'foo' => 'who'})
53
+ assert_equal({'foo' => 'who', 'bar' => nil}, struct_coder.dump(struct))
54
+ struct.bar = 'whar'
55
+ assert_equal({'foo' => 'who', 'bar' => 'whar'}, struct_coder.dump(struct))
56
+ end
57
+ describe 'array' do
58
+ let(:options) { {array: true} }
59
+ it 'dumps an array of structs' do
60
+ structs = [struct.new('x', 'y'), struct.new('z', 'q')]
61
+ assert_equal([{"foo" => "x", "bar" => "y"}, {"foo" => "z", "bar" => "q"}], struct_coder.dump(structs))
62
+ end
63
+ it 'does not dump what is not an array of structs' do
64
+ assert_raises(ARMS::DumpError) { struct_coder.dump(struct.new('z', 'q')) }
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,25 @@
1
+ proc { |p| $:.unshift(p) unless $:.any? { |lp| File.expand_path(lp) == p } }.call(File.expand_path('../lib', File.dirname(__FILE__)))
2
+
3
+ require 'coveralls'
4
+ if Coveralls.will_run?
5
+ Coveralls.wear!
6
+ end
7
+
8
+ require 'simplecov'
9
+ require 'byebug'
10
+
11
+ # NO EXPECTATIONS
12
+ ENV["MT_NO_EXPECTATIONS"] = ''
13
+
14
+ require 'minitest/autorun'
15
+ require 'minitest/reporters'
16
+ Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
17
+
18
+ class ARMSSpec < Minitest::Spec
19
+ end
20
+
21
+ # register this to be the base class for specs instead of Minitest::Spec
22
+ Minitest::Spec.register_spec_type(//, ARMSSpec)
23
+
24
+ require 'arms'
25
+ require_relative 'blog_models'
metadata ADDED
@@ -0,0 +1,168 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: arms
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1.pre.rc1
5
+ platform: ruby
6
+ authors:
7
+ - Ethan
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2019-08-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '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: '2.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '5.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '5.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest-reporters
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: simplecov
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: sqlite3
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.3'
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: 1.3.6
107
+ type: :development
108
+ prerelease: false
109
+ version_requirements: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - "~>"
112
+ - !ruby/object:Gem::Version
113
+ version: '1.3'
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: 1.3.6
117
+ description: A library which offers flexible, chained serializion for Active Record
118
+ email:
119
+ - ethan@unth.net
120
+ executables: []
121
+ extensions: []
122
+ extra_rdoc_files: []
123
+ files:
124
+ - ".simplecov"
125
+ - ".yardopts"
126
+ - LICENSE.txt
127
+ - README.md
128
+ - Rakefile.rb
129
+ - arms.gemspec
130
+ - lib/arms.rb
131
+ - lib/arms/indifferent_hashes_coder.rb
132
+ - lib/arms/multi_coder.rb
133
+ - lib/arms/struct_coder.rb
134
+ - lib/arms/version.rb
135
+ - test/arms_test.rb
136
+ - test/blog_models.rb
137
+ - test/struct_coder_test.rb
138
+ - test/test_helper.rb
139
+ homepage: https://github.com/notEthan/arms
140
+ licenses:
141
+ - MIT
142
+ metadata: {}
143
+ post_install_message:
144
+ rdoc_options: []
145
+ require_paths:
146
+ - lib
147
+ required_ruby_version: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ required_rubygems_version: !ruby/object:Gem::Requirement
153
+ requirements:
154
+ - - ">"
155
+ - !ruby/object:Gem::Version
156
+ version: 1.3.1
157
+ requirements: []
158
+ rubyforge_project:
159
+ rubygems_version: 2.7.8
160
+ signing_key:
161
+ specification_version: 4
162
+ summary: Active Record Multiple Serialization
163
+ test_files:
164
+ - test/arms_test.rb
165
+ - test/blog_models.rb
166
+ - test/struct_coder_test.rb
167
+ - test/test_helper.rb
168
+ - ".simplecov"