son_jay 0.1.0.alpha

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