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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/lib/shape.rb +12 -0
- data/lib/shape/base.rb +130 -0
- data/lib/shape/data_visitor.rb +32 -0
- data/lib/shape/property_shaper.rb +82 -0
- data/lib/shape/renderers.rb +14 -0
- data/lib/shape/version.rb +3 -0
- data/lib/shape/view_decorator.rb +55 -0
- data/shape.gemspec +19 -0
- data/spec/association_shaper_spec.rb +186 -0
- data/spec/base_spec.rb +217 -0
- data/spec/data_visitor_spec.rb +57 -0
- data/spec/property_shaper_spec.rb +139 -0
- data/spec/spec_helper.rb +17 -0
- metadata +85 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
data/lib/shape.rb
ADDED
@@ -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
|
data/lib/shape/base.rb
ADDED
@@ -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,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
|
data/shape.gemspec
ADDED
@@ -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
|
data/spec/base_spec.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|