shape 0.0.1

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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7721ae7dec761e6fc7df7c801c2fa4e2176d07d4
4
+ data.tar.gz: ccbbc447f0038018e40a1f1e75d8e2c4444a5562
5
+ SHA512:
6
+ metadata.gz: 878acd9eab21ba6c2732285de0b2810f71038b801764060ccf29c5eee1222b8e4c7cf5f43cfa80423d90b5d9d40a74ec4d3be0ab64697576fb2e1e4d24698c72
7
+ data.tar.gz: aa47f8d4ac3e7bcd5e08332f111d7b706e63810d70ea8bc86775b9279da5da02e13c22bdcb6f23802b4e525c074085ac1b9b76b3b7feca0d4df0392a646b2288
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :development do
6
+ gem 'awesome_print'
7
+ gem 'rspec'
8
+ gem 'rspec-rails'
9
+ end
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 TODO: Write your name
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,29 @@
1
+ # Shape
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'shape'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install shape
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
@@ -0,0 +1,12 @@
1
+ require 'active_support/all'
2
+ require 'shape/version'
3
+ require 'shape/base'
4
+ require 'shape/property_shaper'
5
+ require 'shape/data_visitor'
6
+ require 'shape/renderers'
7
+
8
+ module Shape
9
+ extend ActiveSupport::Concern
10
+ include Shape::Base
11
+ include Shape::Renderers
12
+ end
@@ -0,0 +1,130 @@
1
+ require 'active_support/concern'
2
+
3
+ module Shape
4
+ module Base
5
+
6
+ extend ActiveSupport::Concern
7
+
8
+ attr_accessor :_source
9
+ attr_accessor :_parent
10
+
11
+ protected :_parent
12
+
13
+ def initialize(source = nil, options = {})
14
+ self._source = source
15
+ self._parent = options.delete(:parent)
16
+ self.delegate_properties_from
17
+ end
18
+
19
+ #def source_name
20
+ #_source.class.name
21
+ #end
22
+ #protected :source_name
23
+ #
24
+
25
+ protected
26
+
27
+ def delegate_properties_from
28
+ self.class._properties_from.each do |from, except|
29
+ Array(_source.send(from)).each do |name|
30
+ unless except.include?(name.to_sym)
31
+ property(name) do
32
+ from do
33
+ _source.send(:[], name)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+
42
+ module ClassMethods
43
+
44
+ def shape(source, options={})
45
+ self.new(source, options)
46
+ end
47
+
48
+ def shaper_context
49
+ @shaper_context || self
50
+ end
51
+
52
+ # Expose a property as {<property_name>: "..."}
53
+ # To expose a property using a different attribute on the resource:
54
+ #
55
+ # property :display, from: :display_name
56
+ #
57
+ # To expose a property with inline definition:
58
+ #
59
+ # property :display do
60
+ # from do
61
+ # #{last_name}, #{first_name}
62
+ # end
63
+ # end
64
+ #
65
+ # To expose a decorated collection:
66
+ #
67
+ # property :practices, with: PracticeDecorator
68
+ #
69
+ #
70
+ # To expose a decorated collection with view context
71
+ #
72
+ # property :practices, with: PracticeDecorator, context: {view: :summary}
73
+ #
74
+ def property(property_name, options={}, &block)
75
+ properties[property_name] = Shape::PropertyShaper.new(
76
+ shaper_context, property_name, options, &block
77
+ )
78
+ end
79
+ alias_method :association, :property
80
+
81
+ def properties_from(name, options={})
82
+ except = Array(options[:except])
83
+ _properties_from << [name, except]
84
+ end
85
+
86
+ def associations
87
+ @associations ||= {}
88
+ end
89
+
90
+ def properties
91
+ @properties ||= {}
92
+ end
93
+
94
+ def _properties_from
95
+ @properties_from ||= []
96
+ end
97
+
98
+ # @overload delegate(*methods, options = {})
99
+ # Overrides {http://api.rubyonrails.org/classes/Module.html#method-i-delegate Module.delegate}
100
+ # to make `:_source` the default delegation target.
101
+ #
102
+ # @return [void]
103
+ def delegate(*methods)
104
+ options = methods.extract_options!
105
+ super *methods, options.reverse_merge(to: :_source)
106
+ end
107
+
108
+ def shape_collection(collection, options = {})
109
+ Array(collection).map do |item|
110
+ self.shape(item, options.clone)
111
+ end
112
+ end
113
+
114
+ protected
115
+ def delegate_property(name)
116
+ if !shaper_context.method_defined?(name)
117
+ shaper_context.delegate(name)
118
+ shaper_context.send(:protected, name)
119
+ end
120
+ end
121
+
122
+
123
+ end
124
+ # Allows properties to be added in an instance..
125
+ include ClassMethods
126
+ def shaper_context
127
+ @shaper_context || self.class
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,32 @@
1
+ module Shape
2
+ module DataVisitor
3
+
4
+ def visit(visitor = lambda {|x| x})
5
+ data_visitor(properties_to_visit, visitor)
6
+ end
7
+
8
+ protected
9
+
10
+ def properties_to_visit
11
+ self.class.properties.merge(self.properties)
12
+ end
13
+
14
+ def data_visitor(properties = self.properties_to_visit, visitor = lambda {|x| x})
15
+ properties.each_with_object({}) do |(name, property), obj|
16
+ if property.options.present? && property.options[:with]
17
+ result = self.send(name)
18
+ if result.respond_to?(:visit)
19
+ obj[name] = result.visit(visitor)
20
+ elsif result.is_a?(Enumerable)
21
+ obj[name] = result.each_with_object([]) do |item, results|
22
+ results << item.visit(visitor)
23
+ end
24
+ end
25
+ else
26
+ obj[name] = visitor.call(self.send(name))
27
+ end
28
+ end
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,82 @@
1
+ module Shape
2
+ # = Property Shaper
3
+ # Keeps track of property info and context
4
+ # when shaping shaper views.
5
+ #
6
+ # We'll use the PropertyShaper objects
7
+ # later to recursively build the data.
8
+ #
9
+ # Allows anything inside a property block
10
+ # to call methods in the context of the
11
+ # Shape dsl allowing for nested properties.
12
+ #
13
+ # Example:
14
+ #
15
+ # property :address do
16
+ # property :street_address do
17
+ # property :addr_line1
18
+ # property :addr_line2
19
+ # end
20
+ # property :city
21
+ # # ...
22
+ # end
23
+ class PropertyShaper
24
+ include Shape::Base::ClassMethods
25
+
26
+ attr_accessor :name
27
+ attr_accessor :shaper_context
28
+ attr_accessor :options
29
+
30
+ def initialize(shaper_context, name, options={}, &block)
31
+ self.shaper_context = shaper_context
32
+ self.name = name
33
+ self.options = options
34
+
35
+ if block
36
+ instance_eval(&block)
37
+ else
38
+ from = options[:from] || name
39
+ define_accessor(name, from)
40
+ delegate_property(from)
41
+ end
42
+ end
43
+
44
+ def from(&block)
45
+ unless shaper_context.method_defined?(name.to_sym)
46
+ shaper_context.send(:define_method, name, &block)
47
+ end
48
+ end
49
+
50
+ protected
51
+
52
+ def define_accessor(name, source_name)
53
+ if !shaper_context.method_defined?(name.to_sym)
54
+ _options = self.options
55
+ self.from do
56
+ return nil unless _source
57
+ result = begin
58
+ _source.send(source_name)
59
+ rescue NoMethodError
60
+ # If source doesn't have a corresponding method, try accessing it
61
+ # via element accessor.
62
+ if _source.respond_to?(:[])
63
+ _source.send(:[], source_name)
64
+ else
65
+ raise
66
+ end
67
+ end
68
+ if with = _options[:with]
69
+ if result.respond_to?(:join)
70
+ with.shape_collection(result, parent: self)
71
+ else
72
+ with.shape(result, parent: self)
73
+ end
74
+ else
75
+ result
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ end
82
+ end
@@ -0,0 +1,14 @@
1
+ module Shape
2
+ module Renderers
3
+ include Shape::DataVisitor
4
+
5
+ def as_json(*args)
6
+ visit(lambda {|x| x.as_json})
7
+ end
8
+
9
+ def to_hash(*args)
10
+ visit
11
+ end
12
+ alias_method :to_h, :to_hash
13
+ end
14
+ end
@@ -0,0 +1,3 @@
1
+ module Shape
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,55 @@
1
+ module Shape
2
+ # = View Decorator
3
+ # Allows for creating different, composable
4
+ # decorator views
5
+ #
6
+ # Example:
7
+ #
8
+ # property :href
9
+ # property :addr_line1
10
+ # property :addr_line2
11
+ # property :city
12
+ # # ...
13
+ #
14
+ # view :with_lat_long do
15
+ # property :latitude
16
+ # property :longitude
17
+ # end
18
+ #
19
+ # view :with_facilities do
20
+ # link :facilities
21
+ # end
22
+ #
23
+ # view :full do
24
+ # view :with_lat_long # nests with_lat_long view
25
+ # view :with_facilities # nests with_facilities view
26
+ # end
27
+ #
28
+ #
29
+ # To use a decorator view, pass the view in to
30
+ # the decorator context. For example:
31
+ #
32
+ # @address = AddressDecorator.new(
33
+ # Address.find(params[:id]),
34
+ # context: {view: :full})
35
+ #
36
+ #
37
+ class ViewDecorator
38
+ include Shape::Base::ClassMethods
39
+ attr_accessor :name
40
+ attr_accessor :decorator_context
41
+ attr_accessor :options
42
+
43
+ def initialize(decorator_context, name, options={}, &block)
44
+ self.decorator_context = decorator_context
45
+ self.name = name
46
+ self.options = options
47
+
48
+ if block
49
+ instance_eval(&block)
50
+ else
51
+ views[name] = decorator_context.views[name]
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require 'shape/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'shape'
7
+ s.version = Shape::VERSION
8
+ s.summary = 'Shape your api'
9
+ s.description = 'Shape your api. Extracted from Vitals Platform.'
10
+ s.authors = ['Robin Curry', 'Brandon Westcott', 'Tim Morgan']
11
+ s.email = ['robin.curry@vitals.com', 'brandon.westcott@vitals.com', 'tim.morgan@vitals.com']
12
+ s.homepage = 'https://github.com/robincurry/shape'
13
+ s.files = `git ls-files`.split("\n")
14
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
15
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
16
+ s.require_paths = ["lib"]
17
+
18
+ s.add_dependency 'activesupport', '>= 3.0'
19
+ end
@@ -0,0 +1,186 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "Property child associations" do
4
+
5
+ context 'Given an object with method attributes' do
6
+
7
+ let(:source) {
8
+ OpenStruct.new.tap do |person|
9
+ person.name = 'John Smith'
10
+ person.age = 34
11
+ person.ssn = 123456789
12
+ person.children = [
13
+ OpenStruct.new.tap do |child|
14
+ child.name = 'Jimmy Smith'
15
+ end,
16
+ OpenStruct.new.tap do |child|
17
+ child.name = 'Jane Smith'
18
+ end,
19
+ ]
20
+ end
21
+ }
22
+
23
+ context 'and Parent and Child Shape decorators' do
24
+
25
+ before do
26
+ stub_const('ChildDecorator', Class.new do
27
+ include Shape::Base
28
+ property :name
29
+ end)
30
+
31
+ stub_const('ParentDecorator', Class.new do
32
+ include Shape::Base
33
+ property :name
34
+ property :years_of_age, from: :age
35
+
36
+ association :children, with: ChildDecorator
37
+ end)
38
+ end
39
+
40
+ context 'when shaped by the decorator' do
41
+
42
+ subject {
43
+ ParentDecorator.new(source)
44
+ }
45
+
46
+ it 'exposes and shapes children associations' do
47
+ expect(subject.children.map(&:name)).to eq(['Jimmy Smith', 'Jane Smith'])
48
+ end
49
+
50
+ end
51
+
52
+ end
53
+
54
+ end
55
+
56
+ context 'Given a hash with attributes' do
57
+
58
+ let(:source) {
59
+ {
60
+ name: 'John Smith',
61
+ age: 34,
62
+ ssn: 123456789,
63
+ children: [
64
+ {
65
+ name: 'Jimmy Smith'
66
+ },
67
+ {
68
+ name: 'Jane Smith'
69
+ }
70
+ ]
71
+ }
72
+ }
73
+
74
+
75
+ context 'and Parent and Child Shape decorators' do
76
+
77
+ before do
78
+ stub_const('ChildDecorator', Class.new do
79
+ include Shape::Base
80
+ property :legal_name, from: :name
81
+ end)
82
+
83
+ stub_const('ParentDecorator', Class.new do
84
+ include Shape::Base
85
+ property :name
86
+ property :years_of_age, from: :age
87
+
88
+ association :children, with: ChildDecorator
89
+ end)
90
+ end
91
+
92
+ context 'when shaped by the decorator' do
93
+
94
+ subject {
95
+ ParentDecorator.new(source)
96
+ }
97
+
98
+ it 'exposes and shapes children associations' do
99
+ expect(subject.children.map(&:legal_name)).to eq(['Jimmy Smith', 'Jane Smith'])
100
+ end
101
+
102
+ end
103
+
104
+ end
105
+
106
+ end
107
+
108
+ context 'Given a hash with deeply nested attributes' do
109
+
110
+ let(:source) {
111
+ {
112
+ name: 'John Smith',
113
+ age: 34,
114
+ ssn: 123456789,
115
+ children: [
116
+ {
117
+ name: 'Jimmy Smith',
118
+ children: [
119
+ {
120
+ name: 'Suzy Smith'
121
+ },
122
+ {
123
+ name: 'Sally Smith'
124
+ }
125
+ ]
126
+ },
127
+ {
128
+ name: 'Jane Smith',
129
+ children: [
130
+ {
131
+ name: 'Sam Smith'
132
+ },
133
+ {
134
+ name: 'Tim Smith'
135
+ },
136
+ ]
137
+ }
138
+ ]
139
+ }
140
+ }
141
+
142
+
143
+ context 'and Parent and Child Shape decorators' do
144
+
145
+ before do
146
+ stub_const('NestedChildDecorator', Class.new do
147
+ include Shape::Base
148
+ property :name
149
+ end)
150
+
151
+ stub_const('ChildDecorator', Class.new do
152
+ include Shape::Base
153
+ property :legal_name, from: :name
154
+ association :children, with: NestedChildDecorator
155
+ end)
156
+
157
+ stub_const('ParentDecorator', Class.new do
158
+ include Shape::Base
159
+ property :name
160
+ property :years_of_age, from: :age
161
+
162
+ association :children, with: ChildDecorator
163
+ end)
164
+ end
165
+
166
+ context 'when shaped by the decorator' do
167
+
168
+ subject {
169
+ ParentDecorator.new(source)
170
+ }
171
+
172
+ it 'exposes and shapes children associations' do
173
+ expect(subject.children.map(&:legal_name)).to eq(['Jimmy Smith', 'Jane Smith'])
174
+ end
175
+
176
+ it 'exposes and shapes nested children associations' do
177
+ expect(subject.children.first.children.map(&:name)).to eq(['Suzy Smith', 'Sally Smith'])
178
+ end
179
+
180
+ end
181
+
182
+ end
183
+
184
+ end
185
+
186
+ end
@@ -0,0 +1,217 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe Shape::Base do
4
+
5
+ context 'Given an object with method attributes' do
6
+
7
+ let(:source) {
8
+ OpenStruct.new.tap do |person|
9
+ person.name = 'John Smith'
10
+ person.age = 34
11
+ person.ssn = 123456789
12
+ person.children = [
13
+ OpenStruct.new.tap do |child|
14
+ child.name = 'Jimmy Smith'
15
+ end,
16
+ OpenStruct.new.tap do |child|
17
+ child.name = 'Jane Smith'
18
+ end,
19
+ ]
20
+ end
21
+ }
22
+
23
+ context 'and a Shape decorator' do
24
+
25
+ before do
26
+ stub_const('MockDecorator', Class.new do
27
+ include Shape::Base
28
+ property :name
29
+ property :years_of_age, from: :age
30
+ end)
31
+
32
+ end
33
+
34
+ context 'when shaped by the decorator' do
35
+
36
+ subject {
37
+ MockDecorator.new(source)
38
+ }
39
+
40
+ it 'exposes defined properties from source' do
41
+ expect(subject.name).to eq('John Smith')
42
+ end
43
+
44
+ it 'exposes defined properties renamed from source' do
45
+ expect(subject.years_of_age).to eq(34)
46
+ end
47
+
48
+ it 'does not expose unspecified attributes' do
49
+ expect(subject).to_not respond_to(:ssn)
50
+ expect(subject).to_not respond_to(:age)
51
+ end
52
+ end
53
+ end
54
+
55
+ context 'and Parent and Child Shape decorators' do
56
+
57
+ before do
58
+ stub_const('ChildDecorator', Class.new do
59
+ include Shape::Base
60
+ property :name
61
+ end)
62
+
63
+ stub_const('ParentDecorator', Class.new do
64
+ include Shape::Base
65
+ property :name
66
+ property :years_of_age, from: :age
67
+
68
+ association :children, with: ChildDecorator
69
+ end)
70
+ end
71
+
72
+ context 'when shaped by the decorator' do
73
+
74
+ subject {
75
+ ParentDecorator.new(source)
76
+ }
77
+
78
+ it 'exposes and shapes children associations' do
79
+ expect(subject.children.map(&:name)).to eq(['Jimmy Smith', 'Jane Smith'])
80
+ end
81
+
82
+ end
83
+
84
+ end
85
+
86
+ end
87
+
88
+ context 'Given a hash with method attributes' do
89
+
90
+ let(:source) {
91
+ {
92
+ name: 'John Smith',
93
+ age: 34,
94
+ ssn: 123456789,
95
+ children: [
96
+ {
97
+ name: 'Jimmy Smith'
98
+ },
99
+ {
100
+ name: 'Jane Smith'
101
+ }
102
+ ]
103
+ }
104
+ }
105
+
106
+ context 'and a Shape decorator' do
107
+
108
+ before do
109
+ stub_const('MockDecorator', Class.new do
110
+ include Shape::Base
111
+ property :name
112
+ property :years_of_age, from: :age
113
+ end)
114
+
115
+ end
116
+
117
+ context 'when shaped by the decorator' do
118
+
119
+ subject {
120
+ MockDecorator.new(source)
121
+ }
122
+
123
+ it 'exposes defined properties from source' do
124
+ expect(subject.name).to eq('John Smith')
125
+ end
126
+
127
+ it 'exposes defined properties renamed from source' do
128
+ expect(subject.years_of_age).to eq(34)
129
+ end
130
+
131
+ it 'does not expose unspecified attributes' do
132
+ expect(subject).to_not respond_to(:ssn)
133
+ expect(subject).to_not respond_to(:age)
134
+ end
135
+ end
136
+
137
+ end
138
+
139
+ context 'and Parent and Child Shape decorators' do
140
+
141
+ before do
142
+ stub_const('ChildDecorator', Class.new do
143
+ include Shape::Base
144
+ property :legal_name, from: :name
145
+ end)
146
+
147
+ stub_const('ParentDecorator', Class.new do
148
+ include Shape::Base
149
+ property :name
150
+ property :years_of_age, from: :age
151
+
152
+ association :children, with: ChildDecorator
153
+ end)
154
+ end
155
+
156
+ context 'when shaped by the decorator' do
157
+
158
+ subject {
159
+ ParentDecorator.new(source)
160
+ }
161
+
162
+ it 'exposes and shapes children associations' do
163
+ expect(subject.children.map(&:legal_name)).to eq(['Jimmy Smith', 'Jane Smith'])
164
+ end
165
+
166
+ end
167
+
168
+ end
169
+
170
+ context 'and a Shape decorator with properties_from' do
171
+
172
+ before do
173
+ stub_const('MockDecorator', Class.new do
174
+ include Shape::Base
175
+ properties_from(:keys)
176
+ end)
177
+
178
+ end
179
+
180
+ context 'when shaped by the decorator' do
181
+
182
+ subject {
183
+ MockDecorator.new(source)
184
+ }
185
+
186
+ it 'exposes defined properties from source for each key' do
187
+ expect(subject).to respond_to(:name, :age, :ssn, :children)
188
+ expect(subject.name).to eq('John Smith')
189
+ end
190
+ end
191
+
192
+ end
193
+
194
+ context 'and a Shape decorator with properties_from with an except list' do
195
+
196
+ before do
197
+ stub_const('MockDecorator', Class.new do
198
+ include Shape::Base
199
+ properties_from(:keys, except: :ssn)
200
+ end)
201
+ end
202
+
203
+ context 'when shaped by the decorator' do
204
+
205
+ subject {
206
+ MockDecorator.new(source)
207
+ }
208
+
209
+ it 'should not expose defined properties for the exceptions' do
210
+ expect(subject).to_not respond_to(:ssn)
211
+ end
212
+ end
213
+
214
+ end
215
+ end
216
+
217
+ end
@@ -0,0 +1,57 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe Shape::DataVisitor do
4
+
5
+ context 'Given a decorated class that implements Shape::DataVisitor' do
6
+
7
+ before do
8
+ stub_const('PersonDecorator', Class.new do
9
+ include Shape::Base
10
+ include Shape::DataVisitor
11
+ property :name
12
+ property :age
13
+ association :spouse, with: self
14
+ end)
15
+ end
16
+
17
+ let(:source) {
18
+ OpenStruct.new.tap do |person|
19
+ person.name = 'John Smith'
20
+ person.age = 34
21
+ person.spouse = OpenStruct.new.tap do |spouse|
22
+ spouse.name = 'Jane Smith'
23
+ spouse.age = 32
24
+ end
25
+ end
26
+ }
27
+
28
+ context 'when I visit the object without providing a visitor' do
29
+ subject {
30
+ PersonDecorator.new(source).visit
31
+ }
32
+
33
+ it 'returns the raw visited data' do
34
+ expect(subject[:name]).to eq('John Smith')
35
+ expect(subject[:age]).to eq(34)
36
+ expect(subject[:spouse][:name]).to eq('Jane Smith')
37
+ expect(subject[:spouse][:age]).to eq(32)
38
+ end
39
+ end
40
+
41
+ context 'when I visit the object with a string visitor' do
42
+ subject {
43
+ PersonDecorator.new(source).visit(lambda do |data|
44
+ data.to_s
45
+ end)
46
+ }
47
+
48
+ it 'returns the visited data as strings' do
49
+ expect(subject[:name]).to eq('John Smith')
50
+ expect(subject[:age]).to eq('34')
51
+ expect(subject[:spouse][:age]).to eq('32')
52
+ end
53
+ end
54
+
55
+ end
56
+
57
+ end
@@ -0,0 +1,139 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe Shape::PropertyShaper do
4
+
5
+ context 'Given an object with method attributes' do
6
+
7
+ let(:source) {
8
+ OpenStruct.new.tap do |person|
9
+ person.name = 'John Smith'
10
+ person.age = 34
11
+ person.ssn = 123456789
12
+ person.children = [
13
+ OpenStruct.new.tap do |child|
14
+ child.name = 'Jimmy Smith'
15
+ end,
16
+ OpenStruct.new.tap do |child|
17
+ child.name = 'Jane Smith'
18
+ end,
19
+ ]
20
+ end
21
+ }
22
+
23
+ context 'and a Shape decorator' do
24
+
25
+ before do
26
+ stub_const('MockDecorator', Class.new do
27
+ include Shape::Base
28
+ property :name
29
+ property :years_of_age, from: :age
30
+ end)
31
+
32
+ end
33
+
34
+ context 'when shaped by the decorator' do
35
+
36
+ subject {
37
+ MockDecorator.new(source)
38
+ }
39
+
40
+ it 'exposes defined properties from source' do
41
+ expect(subject.name).to eq('John Smith')
42
+ end
43
+
44
+ it 'exposes defined properties renamed from source' do
45
+ expect(subject.years_of_age).to eq(34)
46
+ end
47
+
48
+ it 'does not expose unspecified attributes' do
49
+ expect(subject).to_not respond_to(:ssn)
50
+ expect(subject).to_not respond_to(:age)
51
+ end
52
+ end
53
+ end
54
+
55
+ end
56
+
57
+ context 'Given a hash with attributes' do
58
+
59
+ let(:source) do
60
+ {
61
+ name: 'John Smith',
62
+ age: 34,
63
+ ssn: 123456789,
64
+ children: [
65
+ {
66
+ name: 'Jimmy Smith'
67
+ },
68
+ {
69
+ name: 'Jane Smith'
70
+ }
71
+ ]
72
+ }
73
+ end
74
+
75
+ context 'and a Shape decorator' do
76
+
77
+ before do
78
+ stub_const('MockDecorator', Class.new do
79
+ include Shape::Base
80
+ property :name
81
+ property :years_of_age, from: :age
82
+ end)
83
+
84
+ end
85
+
86
+ context 'when shaped by the decorator' do
87
+
88
+ subject {
89
+ MockDecorator.new(source)
90
+ }
91
+
92
+ it 'exposes defined properties from source' do
93
+ expect(subject.name).to eq('John Smith')
94
+ end
95
+
96
+ it 'exposes defined properties renamed from source' do
97
+ expect(subject.years_of_age).to eq(34)
98
+ end
99
+
100
+ it 'does not expose unspecified attributes' do
101
+ expect(subject).to_not respond_to(:ssn)
102
+ expect(subject).to_not respond_to(:age)
103
+ end
104
+ end
105
+
106
+ end
107
+
108
+ context 'and a Shape decorator with property with:' do
109
+
110
+ before do
111
+ stub_const('ChildDecorator', Class.new do
112
+ include Shape::Base
113
+ property :name
114
+ end)
115
+
116
+ stub_const('MockDecorator', Class.new do
117
+ include Shape::Base
118
+ property :children, with: ChildDecorator
119
+ end)
120
+
121
+ end
122
+
123
+ context 'when shaped by the decorator' do
124
+
125
+ subject {
126
+ MockDecorator.new(source)
127
+ }
128
+
129
+ it 'exposes and shapes each child element of the property with the provided decorator' do
130
+ expect(subject.children.map(&:name)).to eq(['Jimmy Smith', 'Jane Smith'])
131
+ end
132
+
133
+ end
134
+
135
+ end
136
+
137
+ end
138
+
139
+ end
@@ -0,0 +1,17 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+
4
+ require 'rspec'
5
+ require 'shape'
6
+
7
+ # Requires supporting files with custom matchers and macros, etc,
8
+ # in ./support/ and its subdirectories.
9
+ # Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
10
+
11
+ RSpec.configure do |config|
12
+ # Use color in STDOUT
13
+ config.color_enabled = true
14
+
15
+ # Use color not only in STDOUT but also in pagers and files
16
+ config.tty = true
17
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shape
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Robin Curry
8
+ - Brandon Westcott
9
+ - Tim Morgan
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2013-09-04 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activesupport
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '3.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - '>='
27
+ - !ruby/object:Gem::Version
28
+ version: '3.0'
29
+ description: Shape your api. Extracted from Vitals Platform.
30
+ email:
31
+ - robin.curry@vitals.com
32
+ - brandon.westcott@vitals.com
33
+ - tim.morgan@vitals.com
34
+ executables: []
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - .gitignore
39
+ - Gemfile
40
+ - Gemfile.lock
41
+ - LICENSE.txt
42
+ - README.md
43
+ - Rakefile
44
+ - lib/shape.rb
45
+ - lib/shape/base.rb
46
+ - lib/shape/data_visitor.rb
47
+ - lib/shape/property_shaper.rb
48
+ - lib/shape/renderers.rb
49
+ - lib/shape/version.rb
50
+ - lib/shape/view_decorator.rb
51
+ - shape.gemspec
52
+ - spec/association_shaper_spec.rb
53
+ - spec/base_spec.rb
54
+ - spec/data_visitor_spec.rb
55
+ - spec/property_shaper_spec.rb
56
+ - spec/spec_helper.rb
57
+ homepage: https://github.com/robincurry/shape
58
+ licenses: []
59
+ metadata: {}
60
+ post_install_message:
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - '>='
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubyforge_project:
76
+ rubygems_version: 2.0.6
77
+ signing_key:
78
+ specification_version: 4
79
+ summary: Shape your api
80
+ test_files:
81
+ - spec/association_shaper_spec.rb
82
+ - spec/base_spec.rb
83
+ - spec/data_visitor_spec.rb
84
+ - spec/property_shaper_spec.rb
85
+ - spec/spec_helper.rb