son_jay 0.1.0.alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,13 @@
1
+ require_relative('../../lib/son_jay')
2
+
3
+ module SonJayFeatureSupport
4
+ def context_data
5
+ @context_data ||= {}
6
+ end
7
+
8
+ def context_module
9
+ @context_module ||= Module.new
10
+ end
11
+ end
12
+
13
+ World(SonJayFeatureSupport)
@@ -0,0 +1,55 @@
1
+ require 'forwardable'
2
+
3
+ module SonJay
4
+
5
+ def self.ModelArray(entry_class)
6
+ Class.new(ModelArray).tap{ |c|
7
+ c.send :entry_class=, entry_class
8
+ }
9
+ end
10
+
11
+ class ModelArray
12
+ extend Forwardable
13
+
14
+ class << self
15
+ attr_accessor :entry_class
16
+
17
+ def array_class
18
+ #TODO: Factor out duplication w/ ObjectModel::array_class
19
+ @array_class ||= begin
20
+ klass = SonJay::ModelArray(self)
21
+ const_set :Array, klass
22
+ end
23
+ end
24
+ end
25
+
26
+ def initialize
27
+ @entries = []
28
+ end
29
+
30
+ def sonj_content
31
+ self
32
+ end
33
+
34
+ def additional
35
+ entry = self.class.entry_class.new
36
+ @entries << entry
37
+ entry
38
+ end
39
+
40
+ def load_data(data)
41
+ data.each do |entry_data|
42
+ additional.sonj_content.load_data entry_data
43
+ end
44
+ end
45
+
46
+ def_delegators :@entries, *[
47
+ :[] ,
48
+ :empty? ,
49
+ :entries ,
50
+ :length ,
51
+ :to_json ,
52
+ ]
53
+
54
+ end
55
+ end
@@ -0,0 +1,70 @@
1
+ require 'forwardable'
2
+
3
+ module SonJay
4
+ class ObjectModel
5
+ class Properties
6
+ class NameError < KeyError
7
+ def initialize(name)
8
+ super "No such property name as %s" % name.inspect
9
+ end
10
+ end
11
+
12
+ extend Forwardable
13
+
14
+ def initialize(property_definitions)
15
+ @data = {}
16
+ @model_properties = Set.new
17
+ property_definitions.each do |d|
18
+ is_model_property = !! d.model_class
19
+ @data[d.name] = is_model_property ? d.model_class.new : nil
20
+ @model_properties << d.name if is_model_property
21
+ end
22
+ end
23
+
24
+ def_delegators :@data, *[
25
+ :length ,
26
+ :values ,
27
+ ]
28
+
29
+ def_delegator :@data, :has_key?, :has_name?
30
+
31
+ def [](name)
32
+ name = "#{name}"
33
+ @data.fetch(name)
34
+ rescue KeyError
35
+ raise NameError.new(name)
36
+ end
37
+
38
+ def []=(name, value)
39
+ name = "#{name}"
40
+ raise NameError.new(name) unless @data.has_key?(name)
41
+ @data[name] = value
42
+ end
43
+
44
+ def load_data(data)
45
+ data.each_pair do |name, value|
46
+ load_property name, value
47
+ end
48
+ end
49
+
50
+ def load_property(name, value)
51
+ name = "#{name}"
52
+ return unless @data.has_key?(name)
53
+ if @model_properties.include?(name)
54
+ @data[name].sonj_content.load_data value
55
+ else
56
+ @data[name] = value
57
+ end
58
+ end
59
+
60
+ def has_name?(name)
61
+ @data.has_key?( "#{name}" )
62
+ end
63
+
64
+ def to_json(*args)
65
+ @data.to_json(*args)
66
+ end
67
+
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,38 @@
1
+ module SonJay
2
+ class ObjectModel
3
+
4
+ class PropertyDefinition
5
+ attr_reader :name, :model_class
6
+
7
+ def initialize(name, instruction = nil)
8
+ @name = name
9
+ @model_class = model_class_for_instruction(instruction)
10
+ end
11
+
12
+ private
13
+
14
+ def model_class_for_instruction(instruction)
15
+ if instruction.nil?
16
+ nil
17
+ elsif instruction.respond_to?(:to_ary)
18
+ array_model_class(instruction)
19
+ elsif instruction.respond_to?( :new )
20
+ instruction
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def array_model_class(instruction)
27
+ return instruction unless instruction.respond_to?(:to_ary)
28
+ return SonJay::ValueArray if instruction == []
29
+
30
+ sub_instruction = instruction.first
31
+ sub_model_class = array_model_class( sub_instruction )
32
+ sub_model_class.array_class
33
+ end
34
+
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,71 @@
1
+ require 'son_jay/object_model/properties'
2
+ require 'son_jay/object_model/property_definition'
3
+
4
+ module SonJay
5
+ class ObjectModel
6
+
7
+ class PropertiesDefiner
8
+
9
+ def initialize(property_definitions)
10
+ @property_definitions = property_definitions
11
+ end
12
+
13
+ def property(name, options={})
14
+ name = "#{name}"
15
+ @property_definitions << PropertyDefinition.new( name, options[:model] )
16
+ end
17
+
18
+ end
19
+
20
+ class << self
21
+
22
+ def json_create(json)
23
+ data = JSON.parse( json )
24
+ instance = new
25
+ instance.sonj_content.load_data data
26
+ instance
27
+ end
28
+
29
+ def properties(&property_initializations)
30
+ @property_initializations = property_initializations
31
+ end
32
+
33
+ def property_definitions
34
+ @property_definitions ||= begin
35
+ definitions = []
36
+
37
+ definer = PropertiesDefiner.new(definitions)
38
+ definer.instance_eval &@property_initializations
39
+ definitions.each do |d|
40
+ name = d.name
41
+ class_eval <<-CODE
42
+ def #{name} ; sonj_content[#{name.inspect}] ; end
43
+ def #{name}=(value) ; sonj_content[#{name.inspect}] = value ; end
44
+ CODE
45
+ end
46
+ definitions
47
+ end
48
+ end
49
+
50
+ def array_class
51
+ @array_class ||= begin
52
+ klass = SonJay::ModelArray(self)
53
+ const_set :Array, klass
54
+ end
55
+ end
56
+
57
+ end
58
+
59
+ attr_reader :sonj_content
60
+
61
+ def initialize
62
+ definitions = self.class.property_definitions
63
+ @sonj_content = ObjectModel::Properties.new( definitions )
64
+ end
65
+
66
+ def to_json(*args)
67
+ sonj_content.to_json(*args)
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,17 @@
1
+ module SonJay
2
+ class ValueArray < ::Array
3
+ Array = SonJay::ModelArray(self)
4
+
5
+ def self.array_class
6
+ self::Array
7
+ end
8
+
9
+ def sonj_content
10
+ self
11
+ end
12
+
13
+ def load_data(data)
14
+ replace data
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ module SonJay
2
+ VERSION = "0.1.0.alpha"
3
+ end
data/lib/son_jay.rb ADDED
@@ -0,0 +1,8 @@
1
+ require 'json'
2
+ require 'son_jay/version'
3
+ require 'son_jay/object_model'
4
+ require 'son_jay/model_array'
5
+ require 'son_jay/value_array'
6
+
7
+ module SonJay
8
+ end
data/son_jay.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'son_jay/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "son_jay"
8
+ spec.version = SonJay::VERSION
9
+ spec.authors = ["Steve Jorgensen"]
10
+ spec.email = ["stevej@stevej.name"]
11
+ spec.summary = %q{Symmetrical transformation between structured data and JSON}
12
+ spec.description = %q{Symmetrical transformation between structured data and JSON}
13
+ spec.homepage = "https://github.com/stevecj/son_jay"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.5"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "rspec"
24
+ spec.add_development_dependency "cucumber"
25
+ end
@@ -0,0 +1,70 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'SonJay::ModelArray()' do
4
+ it "returns a subclass of ModelArray for entries of given type" do
5
+ entry_class_1 = Class.new
6
+ entry_class_2 = Class.new
7
+
8
+ array_model_subclass_1 = SonJay::ModelArray( entry_class_1 )
9
+ array_model_subclass_2 = SonJay::ModelArray( entry_class_2 )
10
+
11
+ expect( array_model_subclass_1.entry_class ).to eq( entry_class_1 )
12
+ expect( array_model_subclass_2.entry_class ).to eq( entry_class_2 )
13
+ end
14
+ end
15
+
16
+ describe SonJay::ModelArray do
17
+ describe "a subclass for entries of a modeled type" do
18
+ let( :subclass ) { Class.new(described_class).tap{ |c|
19
+ c.send :entry_class=, entry_class
20
+ } }
21
+ let( :entry_class ) { Class.new do
22
+ class Content
23
+ def initialize( entry ) ; @entry = entry ; end
24
+ def load_data(data) ; @entry.loaded_data = data ; end
25
+ end
26
+
27
+ attr_accessor :loaded_data
28
+ def sonj_content ; Content.new(self) ; end
29
+ end }
30
+
31
+ describe '::array_class' do
32
+ it "returns a model-array subclass for entries of this model-array type (nested array)" do
33
+ array_class = subclass.array_class
34
+ puts array_class.ancestors
35
+ expect( array_class.ancestors ).to include( SonJay::ModelArray )
36
+ expect( array_class.entry_class ).to eq( subclass )
37
+ end
38
+ end
39
+
40
+ it "produces instances that are initially empty" do
41
+ instance = subclass.new
42
+ expect( instance ).to be_empty
43
+ expect( instance.length ).to eq( 0 )
44
+ end
45
+
46
+ describe "#additional" do
47
+ it "adds a new entry of the modeled type, and returns the entry" do
48
+ instance = subclass.new
49
+
50
+ entry_0 = instance.additional
51
+ entry_1 = instance.additional
52
+
53
+ expect( entry_0 ).to be_kind_of( entry_class )
54
+ expect( instance.entries ).to eq( [entry_0, entry_1] )
55
+ end
56
+ end
57
+
58
+ describe '#load_data' do
59
+ it "loads entries in the given enumerable into its own model instance entries" do
60
+ instance = subclass.new
61
+
62
+ instance.load_data( ['entry 0 data', 'entry 1 data'] )
63
+
64
+ expect( instance.length ).to eq( 2 )
65
+ expect( instance[0].loaded_data ).to eq( 'entry 0 data' )
66
+ expect( instance[1].loaded_data ).to eq( 'entry 1 data' )
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,125 @@
1
+ require 'spec_helper'
2
+
3
+ describe SonJay::ObjectModel::Properties do
4
+ subject{
5
+ described_class.new( [
6
+ SonJay::ObjectModel::PropertyDefinition.new( 'aaa' ) ,
7
+ SonJay::ObjectModel::PropertyDefinition.new( 'bbb' ) ,
8
+ SonJay::ObjectModel::PropertyDefinition.new( 'ccc' ) ,
9
+ SonJay::ObjectModel::PropertyDefinition.new( 'ddd', ddd_model_class ) ,
10
+ ] )
11
+ }
12
+
13
+ let( :ddd_model_class ) { Class.new do
14
+ class Content
15
+ def initialize(model) ; @model = model ; end
16
+ def load_data(data) ; @model.class.loaded_data = data ; end
17
+ end
18
+ class << self ; attr_accessor :loaded_data ; end
19
+ def to_json(*) ; '"ddd..."' ; end
20
+ def sonj_content ; Content.new(self) ; end
21
+ end }
22
+
23
+ it "has an entry for each property name specified during initialization" do
24
+ expect( subject.length ).to eq( 4 )
25
+ expect( subject ).to have_name('aaa')
26
+ expect( subject ).to have_name('bbb')
27
+ expect( subject ).to have_name('ccc')
28
+ expect( subject ).to have_name('ddd')
29
+ end
30
+
31
+ describe "property value access by name" do
32
+ it "reads nil by default for an existing value property" do
33
+ expect( subject['aaa'] ).to be_nil
34
+ end
35
+
36
+ it "writes and reads existing value properties" do
37
+ subject['bbb'] = 10
38
+ subject['aaa'] = 11
39
+
40
+ expect( subject['bbb'] ).to eq( 10 )
41
+ expect( subject['aaa'] ).to eq( 11 )
42
+ end
43
+
44
+ it "refuses to read a non-existent property" do
45
+ expect{ subject['abc'] }.to raise_exception( described_class::NameError )
46
+ end
47
+
48
+ it "refuses to write a non-existent property" do
49
+ expect{ subject['abc'] = 1 }.to raise_exception( described_class::NameError )
50
+ end
51
+
52
+ it "allows reading/writing by symbol or string for property name" do
53
+ subject['aaa'] = 10
54
+ subject[:bbb] = 11
55
+
56
+ expect( subject[:aaa] ).to eq( 10 )
57
+ expect( subject['bbb'] ).to eq( 11 )
58
+ end
59
+
60
+ it "reads an instance of the model for a modeled attribute" do
61
+ expect( subject[:ddd] ).to be_kind_of( ddd_model_class )
62
+ end
63
+ end
64
+
65
+ describe "#to_json" do
66
+ it "returns a JSON object representation with attribute values" do
67
+ subject['aaa'] = 'abc'
68
+ subject['ccc'] = true
69
+
70
+ actual_json = subject.to_json
71
+
72
+ actual_data = JSON.parse( actual_json )
73
+ expected_data = {
74
+ 'aaa' => 'abc' ,
75
+ 'bbb' => nil ,
76
+ 'ccc' => true ,
77
+ 'ddd' => "ddd..." ,
78
+ }
79
+ expect( actual_data ).to eq( expected_data )
80
+ end
81
+ end
82
+
83
+ describe "load_property" do
84
+ it "writes to existing value properties" do
85
+ subject.load_property( 'bbb', 11 )
86
+ subject.load_property( 'ccc', 12 )
87
+
88
+ expect( subject['bbb'] ).to eq( 11 )
89
+ expect( subject['ccc'] ).to eq( 12 )
90
+ end
91
+
92
+ it "loads data into existing modeled properties" do
93
+ subject.load_property( 'ddd', 'some data' )
94
+ expect( ddd_model_class.loaded_data ).to eq( 'some data' )
95
+ end
96
+
97
+ it "ignores attempts to write to non-existent properties" do
98
+ expect{ subject.load_property('xx', 10) }.
99
+ not_to change{ subject.length }
100
+ end
101
+
102
+ it "allows string or symbol for property name" do
103
+ subject.load_property( 'aaa' , 888 )
104
+ subject.load_property( :ccc , 999 )
105
+
106
+ expect( subject['aaa'] ).to eq( 888 )
107
+ expect( subject['ccc'] ).to eq( 999 )
108
+ end
109
+ end
110
+
111
+ describe "load_data" do
112
+ it "populates property values from hash entries" do
113
+ subject.load_data({
114
+ 'bbb' => 'abc' ,
115
+ 'ccc' => false ,
116
+ 'ddd' => 'something...' ,
117
+ })
118
+ expect( subject['aaa'] ).to be_nil
119
+ expect( subject['bbb'] ).to eq( 'abc' )
120
+ expect( subject['ccc'] ).to eq( false )
121
+ expect( ddd_model_class.loaded_data ).to eq( 'something...' )
122
+ end
123
+ end
124
+
125
+ end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ describe SonJay::ObjectModel::PropertyDefinition do
4
+
5
+ it "has a nil model class for a nil instruction" do
6
+ instance = described_class.new( :a, nil )
7
+ expect( instance.model_class ).to be_nil
8
+ end
9
+
10
+ it "has a model class for a model class instruction" do
11
+ model_class = Class.new
12
+
13
+ instance = described_class.new( :a, model_class )
14
+ expect( instance.model_class ).to eq( model_class )
15
+ end
16
+
17
+ it "has a value-array class for an empty-array instruction" do
18
+ instance = described_class.new( :a, [] )
19
+ expect( instance.model_class ).to eq( SonJay::ValueArray )
20
+ end
21
+
22
+ it "has a model-array class for an array w/ model class entry" do
23
+ entry_model_class = Class.new do
24
+ class Array ; end
25
+ def self.array_class ; Array ; end
26
+ end
27
+
28
+ instance = described_class.new( :a, [ entry_model_class ] )
29
+ expect( instance.model_class ).to eq( entry_model_class::Array )
30
+ end
31
+ end
@@ -0,0 +1,136 @@
1
+ require 'spec_helper'
2
+
3
+ describe SonJay::ObjectModel do
4
+
5
+ describe "a subclass that defines value properties" do
6
+
7
+ describe '::array_class' do
8
+ it "returns a model-array subclass for entries of this type" do
9
+ array_class = subclass.array_class
10
+ puts array_class.ancestors
11
+ expect( array_class.ancestors ).to include( SonJay::ModelArray )
12
+ expect( array_class.entry_class ).to eq( subclass )
13
+ end
14
+ end
15
+
16
+ let( :subclass ) {
17
+ Class.new(described_class) do
18
+ properties do
19
+ property :abc
20
+ property :xyz
21
+ end
22
+ end
23
+ }
24
+
25
+ describe "#sonj_content" do
26
+ it "has name-indexed settable/gettable values for defined properties" do
27
+ instance = subclass.new
28
+
29
+ content = instance.sonj_content
30
+ expect( content.length ).to eq( 2 )
31
+
32
+ content[:abc] = 1
33
+ content[:xyz] = 'XYZ'
34
+
35
+ expect( content[:abc] ).to eq( 1 )
36
+ expect( content[:xyz] ).to eq( 'XYZ' )
37
+ end
38
+ end
39
+
40
+ it "has direct property accessor methods for each property" do
41
+ instance = subclass.new
42
+ instance.abc, instance.xyz = 11, 22
43
+
44
+ expect( [instance.abc, instance.xyz] ).to eq( [11, 22] )
45
+ end
46
+
47
+ it "serializes to a JSON object representation w/ property values" do
48
+ instance = subclass.new
49
+ instance.abc, instance.xyz = 'ABC', nil
50
+
51
+ actual_json = instance.to_json
52
+
53
+ actual_data = JSON.parse( actual_json)
54
+ expected_data = {'abc' => 'ABC', 'xyz' => nil}
55
+ expect( actual_data ).to eq( expected_data )
56
+ end
57
+
58
+ describe '::json_create' do
59
+ it "creates an instance with properties filled in from parsed JSON" do
60
+ json = <<-JSON
61
+ {
62
+ "abc": 123 ,
63
+ "xyz": "XYZ"
64
+ }
65
+ JSON
66
+
67
+ instance = subclass.json_create( json )
68
+
69
+ expect( instance.abc ).to eq( 123 )
70
+ expect( instance.xyz ).to eq( 'XYZ' )
71
+ end
72
+ end
73
+
74
+ end
75
+
76
+ describe "a subclass that defines value and modeled-object properties" do
77
+ let( :subclass ) {
78
+ dmc_1, dmc_2 = detail_model_class_1, detail_model_class_2
79
+ pbcs = property_block_calls
80
+ Class.new(described_class) do
81
+ properties do
82
+ pbcs << 1
83
+ property :a
84
+ property :obj_1, model: dmc_1
85
+ property :obj_2, model: dmc_2
86
+ end
87
+ end
88
+ }
89
+
90
+ let( :property_block_calls ) { [] }
91
+
92
+ let( :detail_model_class_1 ) {
93
+ Class.new(described_class) do
94
+ properties do
95
+ property :aaa
96
+ property :bbb
97
+ end
98
+ end
99
+ }
100
+
101
+ let( :detail_model_class_2 ) {
102
+ Class.new(described_class) do
103
+ properties do
104
+ property :ccc
105
+ end
106
+ end
107
+ }
108
+
109
+ it "does not immediately invoke its properties block when declared" do
110
+ _ = subclass
111
+ expect( property_block_calls ).to be_empty
112
+ end
113
+
114
+ describe "#sonj_content" do
115
+ it "has an entry for each defined property" do
116
+ content = subclass.new.sonj_content
117
+ expect( content.length ).to eq( 3 )
118
+ end
119
+ end
120
+
121
+ describe "#sonj_content" do
122
+ it "has name-indexed settable/gettable values for defined value properties" do
123
+ content = subclass.new.sonj_content
124
+ content[:a] = 1
125
+ expect( content[:a] ).to eq( 1 )
126
+ end
127
+
128
+ it "has name-indexed gettable values for defined modeled-object properties" do
129
+ content = subclass.new.sonj_content
130
+ expect( content[:obj_1] ).to be_kind_of( detail_model_class_1 )
131
+ end
132
+ end
133
+
134
+ end
135
+
136
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe SonJay do
4
+ it 'has a version number' do
5
+ SonJay::VERSION.should_not be_nil
6
+ end
7
+ end
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'son_jay'