model_view 0.0.0 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gem_server +1 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +3 -0
- data/lib/model_view/resolver.rb +58 -0
- data/lib/model_view/version.rb +3 -0
- data/lib/model_view.rb +56 -0
- data/model_view.gemspec +26 -0
- data/spec/model_view/model_view_spec.rb +146 -0
- data/spec/model_view/resolver_spec.rb +135 -0
- data/spec/spec_helper.rb +6 -0
- metadata +19 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e1dc28eafd2cab5fd5c515e0a011d19240bb9244
|
4
|
+
data.tar.gz: 4983872ab1e955511334fae65c404d0348d5cae2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4a922409b4e094409d70a5aafd16ed365f5b9ca97cd2f4350bb5a27b373de46972b4d4dd2535fee6b4dfde9367505a92dcdc35b6e50fe31e859c2fda4b4942da
|
7
|
+
data.tar.gz: 63101e9b84430de3fb973d28301e921fcb743a22ca6feb0c6d38481b7b9259cc6b307c1efabf7d1fd43ae7014773a69066d1eb7498911c0b8ed37f85118637bb
|
data/.gem_server
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rubygems.org
|
data/Gemfile
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
module ModelView
|
2
|
+
class Resolver
|
3
|
+
|
4
|
+
class << self
|
5
|
+
def resolve(obj, scopes, scope=nil, context={})
|
6
|
+
scope ||= ModelView::ROOT
|
7
|
+
|
8
|
+
fields = fields_for_scope(scope, scopes)
|
9
|
+
|
10
|
+
fields.each_with_object({}) do |(field_name, field_data), result|
|
11
|
+
result[field_name] = evaluate_field(obj, field_name, field_data[:args], field_data[:block], context)
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
def fields_for_scope(scope, scope_data)
|
17
|
+
root_scope_fields = extract_fields(ModelView::ROOT, scope_data)
|
18
|
+
scope_fields = scope == ModelView::ROOT ? {} : extract_fields(scope, scope_data)
|
19
|
+
|
20
|
+
extended_scopes = scope_data[scope][:extends] || []
|
21
|
+
extended_fields = extended_scopes.reduce({}) do |res, scope|
|
22
|
+
res.merge(extract_fields(scope, scope_data))
|
23
|
+
end
|
24
|
+
|
25
|
+
included_scopes = (scope_data[scope][:includes] || []).reduce({}) do |res, scope_name|
|
26
|
+
res[scope_name] = fields_for_scope(scope_name, scope_data)
|
27
|
+
res
|
28
|
+
end
|
29
|
+
|
30
|
+
{}.merge(root_scope_fields)
|
31
|
+
.merge(scope_fields)
|
32
|
+
.merge(extended_fields)
|
33
|
+
.merge(included_scopes)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def evaluate_field(object, field_name, options, block, context)
|
39
|
+
if block.nil?
|
40
|
+
object.send(field_name)
|
41
|
+
else
|
42
|
+
if block.arity == 1
|
43
|
+
block.call(object)
|
44
|
+
else
|
45
|
+
arguments_for_block = options[:context].map { |key| context[key] }
|
46
|
+
block.call(object, *arguments_for_block)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def extract_fields(scope, scope_data)
|
52
|
+
scope_data[scope][:fields]
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
data/lib/model_view.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'model_view/resolver'
|
2
|
+
|
3
|
+
module ModelView
|
4
|
+
|
5
|
+
ROOT = :__root__
|
6
|
+
|
7
|
+
def field(field_name, arg={}, &block)
|
8
|
+
scope_name = @current_scope || ROOT
|
9
|
+
add_field scope_name, field_name, arg, block
|
10
|
+
end
|
11
|
+
|
12
|
+
def fields(*fields)
|
13
|
+
fields.flatten.each { |f| field f }
|
14
|
+
end
|
15
|
+
|
16
|
+
def scope(scope_name, &block)
|
17
|
+
sym_scope_name = scope_name.to_sym
|
18
|
+
add_scope(sym_scope_name)
|
19
|
+
|
20
|
+
@current_scope = sym_scope_name
|
21
|
+
instance_eval &block
|
22
|
+
@current_scope = nil
|
23
|
+
end
|
24
|
+
|
25
|
+
def include_scope(*scope)
|
26
|
+
raise Exception.new("Root scope can not include another scope") if @current_scope.nil? || @current_scope == ROOT
|
27
|
+
scope.flatten.each { |s| @scopes[@current_scope][:includes] << s }
|
28
|
+
end
|
29
|
+
|
30
|
+
def extend_scope(*scope)
|
31
|
+
raise Exception.new("Root scope can not extend another scope") if @current_scope.nil? || @current_scope == ROOT
|
32
|
+
scope.flatten.each { |s| @scopes[@current_scope][:extends] << s }
|
33
|
+
end
|
34
|
+
|
35
|
+
def scopes
|
36
|
+
@scopes
|
37
|
+
end
|
38
|
+
|
39
|
+
def as_hash(object, scope=nil, context={})
|
40
|
+
ModelView::Resolver.resolve(object, @scopes, scope || ROOT, context)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def add_scope(scope_name)
|
46
|
+
@scopes ||= {}
|
47
|
+
@scopes[scope_name] = {fields: {}, extends: [], includes: []}
|
48
|
+
end
|
49
|
+
|
50
|
+
def add_field(scope, field_name, args, block)
|
51
|
+
@scopes ||= {}
|
52
|
+
@scopes[scope] ||= {fields: {}, extends: [], includes: []}
|
53
|
+
@scopes[scope][:fields][field_name] = {args: args, block: block}
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
data/model_view.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'model_view/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'model_view'
|
7
|
+
spec.version = ModelView::VERSION
|
8
|
+
spec.date = '2017-06-13'
|
9
|
+
spec.summary = "Composable serialisation for models"
|
10
|
+
spec.description = "Composable serialisation for models"
|
11
|
+
spec.authors = ["Martin Pretorius"]
|
12
|
+
spec.email = ['martin@offerzen.com', 'glasnoster@gmail.com']
|
13
|
+
spec.homepage = 'https://github.com/Offerzen/model_view'
|
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 "semvergen", "~> 1.9"
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
24
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
25
|
+
spec.add_development_dependency "pry", "~> 0.10"
|
26
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'pry'
|
3
|
+
|
4
|
+
describe ModelView do
|
5
|
+
let(:dummy_class) { Class.new { extend ModelView } }
|
6
|
+
let(:root_scope) { :__root__ }
|
7
|
+
|
8
|
+
context "root scope" do
|
9
|
+
describe :field do
|
10
|
+
context "without a block" do
|
11
|
+
it "adds the field to the root scope" do
|
12
|
+
dummy_class.field :a_field
|
13
|
+
|
14
|
+
scope_fields = dummy_class.scopes[root_scope][:fields]
|
15
|
+
expect(scope_fields[:a_field]).to eq({args: {}, block: nil})
|
16
|
+
end
|
17
|
+
|
18
|
+
context "with arguments" do
|
19
|
+
it "adds the field to the root scope" do
|
20
|
+
dummy_class.field :a_field, {foo: 1}
|
21
|
+
|
22
|
+
scope_fields = dummy_class.scopes[root_scope][:fields]
|
23
|
+
expect(scope_fields[:a_field]).to eq({args: {foo: 1}, block: nil})
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context "with a block" do
|
29
|
+
it "adds the field to the root scope" do
|
30
|
+
dummy_class.field :a_field { 1 + 1 }
|
31
|
+
|
32
|
+
scope_fields = dummy_class.scopes[root_scope][:fields]
|
33
|
+
|
34
|
+
expect(scope_fields[:a_field][:args]).to eq({})
|
35
|
+
expect(scope_fields[:a_field][:block].call).to eq(2)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe :fields do
|
41
|
+
it "adds all the fields to the root scope" do
|
42
|
+
dummy_class.fields :field_1, :field_2, :field_3
|
43
|
+
|
44
|
+
scope_fields = dummy_class.scopes[root_scope][:fields]
|
45
|
+
expect(scope_fields.keys).to eq([:field_1, :field_2, :field_3])
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe :include_scope do
|
50
|
+
it "raises an error" do
|
51
|
+
expect { dummy_class.include_scope :a_field }.to raise_error "Root scope can not include another scope"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe :extend_scope do
|
56
|
+
it "raises an error" do
|
57
|
+
expect { dummy_class.extend_scope :a_field }.to raise_error "Root scope can not extend another scope"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context "within a scope" do
|
63
|
+
describe :scope do
|
64
|
+
it "creates a scope" do
|
65
|
+
dummy_class.scope :a_scope { }
|
66
|
+
|
67
|
+
expect(dummy_class.scopes.keys).to include(:a_scope)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe :field do
|
72
|
+
it "adds a field inside the scope" do
|
73
|
+
dummy_class.scope :a_scope { field :foo }
|
74
|
+
|
75
|
+
scope_fields = dummy_class.scopes[:a_scope][:fields]
|
76
|
+
expect(scope_fields.keys).to include(:foo)
|
77
|
+
end
|
78
|
+
|
79
|
+
it "changes the current scope back to root" do
|
80
|
+
dummy_class.scope :a_scope { field :foo }
|
81
|
+
dummy_class.field :bar
|
82
|
+
|
83
|
+
scope_fields = dummy_class.scopes[:a_scope][:fields]
|
84
|
+
root_scope_fields = dummy_class.scopes[root_scope][:fields]
|
85
|
+
|
86
|
+
expect(scope_fields.keys).to include(:foo)
|
87
|
+
expect(root_scope_fields.keys).to include(:bar)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe :fields do
|
92
|
+
it "adds all the fields to the current scope" do
|
93
|
+
dummy_class.scope :foo_scope { fields :field_1, :field_2, :field_3 }
|
94
|
+
|
95
|
+
scope_fields = dummy_class.scopes[:foo_scope][:fields]
|
96
|
+
expect(scope_fields.keys).to eq([:field_1, :field_2, :field_3])
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
describe :include_scope do
|
101
|
+
context "given a single scope" do
|
102
|
+
it "adds the scope to the current scope's includes" do
|
103
|
+
dummy_class.scope :my_scope { include_scope :foo }
|
104
|
+
expect(dummy_class.scopes[:my_scope][:includes]).to eq([:foo])
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
context "given an array" do
|
109
|
+
it "adds all the scopes to the current scope's includes" do
|
110
|
+
dummy_class.scope :my_scope { include_scope [:foo, :bar] }
|
111
|
+
expect(dummy_class.scopes[:my_scope][:includes]).to eq([:foo, :bar])
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
context "given more than one scope" do
|
116
|
+
it "adds all the scopes to the current scope's includes" do
|
117
|
+
dummy_class.scope :my_scope { include_scope :foo, :bar }
|
118
|
+
expect(dummy_class.scopes[:my_scope][:includes]).to eq([:foo, :bar])
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
describe :extend_scope do
|
124
|
+
context "given a single scope" do
|
125
|
+
it "adds the scope to the current scope's extends" do
|
126
|
+
dummy_class.scope :my_scope { extend_scope :foo }
|
127
|
+
expect(dummy_class.scopes[:my_scope][:extends]).to eq([:foo])
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
context "given an array" do
|
132
|
+
it "adds all the scopes to the current scope's extends" do
|
133
|
+
dummy_class.scope :my_scope { extend_scope [:foo, :bar] }
|
134
|
+
expect(dummy_class.scopes[:my_scope][:extends]).to eq([:foo, :bar])
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
context "given more than one scope" do
|
139
|
+
it "adds all the scopes to the current scope's extends" do
|
140
|
+
dummy_class.scope :my_scope { extend_scope :foo, :bar }
|
141
|
+
expect(dummy_class.scopes[:my_scope][:extends]).to eq([:foo, :bar])
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'pry'
|
3
|
+
|
4
|
+
describe ModelView::Resolver do
|
5
|
+
let(:scopes) do
|
6
|
+
{
|
7
|
+
__root__: {
|
8
|
+
fields: {
|
9
|
+
field1: {},
|
10
|
+
field2: {},
|
11
|
+
field3: {}
|
12
|
+
},
|
13
|
+
},
|
14
|
+
scope1: {
|
15
|
+
fields: {
|
16
|
+
field4: {block: Proc.new { |obj| obj.field4 + obj.field1 }},
|
17
|
+
field5: {},
|
18
|
+
field6: {}
|
19
|
+
},
|
20
|
+
},
|
21
|
+
scope2: {
|
22
|
+
extends: [:scope1],
|
23
|
+
fields: {
|
24
|
+
field7: {block: Proc.new { |obj, counter| obj.field7 + counter}, args: {context: [:my_counter]}},
|
25
|
+
field8: {},
|
26
|
+
field9: {}
|
27
|
+
},
|
28
|
+
},
|
29
|
+
scope3: {
|
30
|
+
includes: [:scope1],
|
31
|
+
fields: {
|
32
|
+
field10: {},
|
33
|
+
field11: {},
|
34
|
+
field12: {}
|
35
|
+
},
|
36
|
+
}
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
describe :fields_for_scope do
|
41
|
+
|
42
|
+
context "on the root scope" do
|
43
|
+
it "returns the root-level fields" do
|
44
|
+
result = ModelView::Resolver.fields_for_scope(ModelView::ROOT, scopes)
|
45
|
+
|
46
|
+
expect(result.keys).to include(*[:field1, :field2, :field3])
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context "on a scope" do
|
51
|
+
it "returns the root-level fields and the scope fields" do
|
52
|
+
result = ModelView::Resolver.fields_for_scope(:scope1, scopes)
|
53
|
+
|
54
|
+
expect(result.keys).to include(*[:field1, :field2, :field3, :field4, :field5, :field6])
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context "on a scope extending another scope" do
|
59
|
+
it "also includes the extended scope's fields" do
|
60
|
+
result = ModelView::Resolver.fields_for_scope(:scope2, scopes)
|
61
|
+
|
62
|
+
expect(result.keys).to include(*[:field1, :field2, :field3, :field4, :field5, :field6, :field7, :field8, :field9])
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
context "on a scope including another scope" do
|
67
|
+
it "includes the includes scope's fields under a namespace" do
|
68
|
+
result = ModelView::Resolver.fields_for_scope(:scope3, scopes)
|
69
|
+
|
70
|
+
expect(result.keys).to include(*[:field1, :field2, :field3, :field10, :field11, :field12, :scope1])
|
71
|
+
expect(result[:scope1].keys).to include(*[:field1, :field2, :field3, :field4, :field5, :field6])
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
describe :resolve do
|
78
|
+
before do
|
79
|
+
class Dummy
|
80
|
+
def field1() 1 end
|
81
|
+
def field2() 2 end
|
82
|
+
def field3() 3 end
|
83
|
+
def field4() 4 end
|
84
|
+
def field5() 5 end
|
85
|
+
def field6() 6 end
|
86
|
+
def field7() 7 end
|
87
|
+
def field8() 8 end
|
88
|
+
def field9() 9 end
|
89
|
+
def field10() 10 end
|
90
|
+
def field11() 11 end
|
91
|
+
def field12() 12 end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
let(:instance) { Dummy.new }
|
96
|
+
|
97
|
+
context "for the root scope" do
|
98
|
+
it "returns the resolved root-scope fields" do
|
99
|
+
res = ModelView::Resolver.resolve(instance, scopes)
|
100
|
+
expect(res).to eq({field1: 1, field2: 2, field3: 3})
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
context "fields with a block" do
|
105
|
+
context "with an arity of one" do
|
106
|
+
it "evaluates the block" do
|
107
|
+
res = ModelView::Resolver.resolve(instance, scopes, :scope1)
|
108
|
+
expect(res[:field4]).to eq(5)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
context "with an arity greater than one" do
|
113
|
+
context "no context specified in the field arguments" do
|
114
|
+
it "foo"
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
context "no a context specified in the field arguments" do
|
119
|
+
let(:dummy_context) { {foo: 1, bar: 2, my_counter: 100} }
|
120
|
+
it "injects the context into the proc" do
|
121
|
+
res = ModelView::Resolver.resolve(instance, scopes, :scope2, dummy_context)
|
122
|
+
expect(res[:field7]).to eq(107)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
it "evaluates the block" do
|
127
|
+
res = ModelView::Resolver.resolve(instance, scopes, :scope1)
|
128
|
+
expect(res[:field4]).to eq(5)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
|
134
|
+
end
|
135
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: model_view
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Martin Pretorius
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '1.
|
19
|
+
version: '1.9'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '1.
|
26
|
+
version: '1.9'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -82,15 +82,26 @@ dependencies:
|
|
82
82
|
version: '0.10'
|
83
83
|
description: Composable serialisation for models
|
84
84
|
email:
|
85
|
+
- martin@offerzen.com
|
85
86
|
- glasnoster@gmail.com
|
86
87
|
executables: []
|
87
88
|
extensions: []
|
88
89
|
extra_rdoc_files: []
|
89
90
|
files:
|
91
|
+
- ".gem_server"
|
90
92
|
- ".gitignore"
|
93
|
+
- CHANGELOG.md
|
94
|
+
- Gemfile
|
91
95
|
- LICENSE
|
92
96
|
- README.md
|
93
|
-
|
97
|
+
- lib/model_view.rb
|
98
|
+
- lib/model_view/resolver.rb
|
99
|
+
- lib/model_view/version.rb
|
100
|
+
- model_view.gemspec
|
101
|
+
- spec/model_view/model_view_spec.rb
|
102
|
+
- spec/model_view/resolver_spec.rb
|
103
|
+
- spec/spec_helper.rb
|
104
|
+
homepage: https://github.com/Offerzen/model_view
|
94
105
|
licenses:
|
95
106
|
- MIT
|
96
107
|
metadata: {}
|
@@ -114,4 +125,7 @@ rubygems_version: 2.6.10
|
|
114
125
|
signing_key:
|
115
126
|
specification_version: 4
|
116
127
|
summary: Composable serialisation for models
|
117
|
-
test_files:
|
128
|
+
test_files:
|
129
|
+
- spec/model_view/model_view_spec.rb
|
130
|
+
- spec/model_view/resolver_spec.rb
|
131
|
+
- spec/spec_helper.rb
|