cistern 2.5.0 → 2.6.0
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 +4 -4
- data/CHANGELOG.md +12 -1
- data/README.md +51 -3
- data/lib/cistern.rb +1 -0
- data/lib/cistern/associations.rb +76 -0
- data/lib/cistern/collection.rb +1 -0
- data/lib/cistern/model.rb +1 -0
- data/lib/cistern/version.rb +1 -1
- data/spec/associations_spec.rb +155 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2f58960f3c738abbdd7203d8973981f71c370a65
|
4
|
+
data.tar.gz: 94a23880c49416884ece0e3615ca7e170bba7234
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 56fd5780ad10f6c44ff6704743cf927bb50600b3c45a00d1aa1afe8c2d4e237f5413bdc416e282255f924763c3fc19d2bc0afd11c52bcfb7ecf7e2f3e6c39fbd
|
7
|
+
data.tar.gz: aee0f212eb1923eb92131faa80a5a8fa1c8ae021e0fd1c485fb5b9ac653fba99558d71757d77fdd9baf9558dbdf27d24b72d11c5ce1cd55e52f6c2aee1fbc771
|
data/CHANGELOG.md
CHANGED
@@ -2,7 +2,18 @@
|
|
2
2
|
|
3
3
|
## [Unreleased](https://github.com/lanej/cistern/tree/HEAD)
|
4
4
|
|
5
|
-
[Full Changelog](https://github.com/lanej/cistern/compare/v2.
|
5
|
+
[Full Changelog](https://github.com/lanej/cistern/compare/v2.5.0...HEAD)
|
6
|
+
|
7
|
+
**Implemented enhancements:**
|
8
|
+
|
9
|
+
- Associations [\#66](https://github.com/lanej/cistern/pull/66) ([lanej](https://github.com/lanej))
|
10
|
+
|
11
|
+
**Fixed bugs:**
|
12
|
+
|
13
|
+
- fix\(model\): \#has\_many is not loaded without records present [\#67](https://github.com/lanej/cistern/pull/67) ([lanej](https://github.com/lanej))
|
14
|
+
|
15
|
+
## [v2.5.0](https://github.com/lanej/cistern/tree/v2.5.0) (2016-07-19)
|
16
|
+
[Full Changelog](https://github.com/lanej/cistern/compare/v2.4.1...v2.5.0)
|
6
17
|
|
7
18
|
**Implemented enhancements:**
|
8
19
|
|
data/README.md
CHANGED
@@ -404,14 +404,62 @@ class Blog::Posts < Blog::Collection
|
|
404
404
|
end
|
405
405
|
```
|
406
406
|
|
407
|
+
### Associations
|
408
|
+
|
409
|
+
Associations allow the use of a resource's attributes to reference other resources. They act as lazy loaded attributes
|
410
|
+
and push any loaded data into the resource's `attributes`.
|
411
|
+
|
412
|
+
There are two types of associations available.
|
413
|
+
|
414
|
+
* `belongs_to` references a specific resource and defines a reader.
|
415
|
+
* `has_many` references a collection of resources and defines a reader / writer.
|
416
|
+
|
417
|
+
```ruby
|
418
|
+
class Blog::Tag < Blog::Model
|
419
|
+
identity :id
|
420
|
+
attribute :author_id
|
421
|
+
|
422
|
+
has_many :posts -> { cistern.posts(tag_id: identity) }
|
423
|
+
belongs_to :creator -> { cistern.authors.get(author_id) }
|
424
|
+
end
|
425
|
+
```
|
426
|
+
|
427
|
+
Relationships store the collection's attributes within the resources' attributes on write / load.
|
428
|
+
|
429
|
+
```ruby
|
430
|
+
tag = blog.tags.get('ruby')
|
431
|
+
tag.posts = blog.posts.load({'id' => 1, 'author_id' => '2'}, {'id' => 2, 'author_id' => 3})
|
432
|
+
tag.attributes[:posts] #=> {'id' => 1, 'author_id' => '2'}, {'id' => 2, 'author_id' => 3}
|
433
|
+
|
434
|
+
tag.creator = blogs.author.get(name: 'phil')
|
435
|
+
tag.attributes[:creator] #=> { 'id' => 2, 'name' => 'phil' }
|
436
|
+
```
|
437
|
+
|
438
|
+
Foreign keys can be updated with association writing by overwriting the writer.
|
439
|
+
|
440
|
+
```ruby
|
441
|
+
Blog::Tag.class_eval do
|
442
|
+
alias cistern_creator= creator=
|
443
|
+
def creator=(creator)
|
444
|
+
self.cistern_creator = creator
|
445
|
+
self.author_id = attributes[:creator][:id]
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
tag = blog.tags.get('ruby')
|
450
|
+
tag.author_id = 4
|
451
|
+
tag.creator = blogs.author.get(name: 'phil') #=> #<Blog::Author id=2 name='phil'>
|
452
|
+
tag.author_id #=> 2
|
453
|
+
```
|
454
|
+
|
407
455
|
#### Data
|
408
456
|
|
409
457
|
A uniform interface for mock data is mixed into the `Mock` class by default.
|
410
458
|
|
411
459
|
```ruby
|
412
460
|
Blog.mock!
|
413
|
-
client = Blog.new
|
414
|
-
client.data
|
461
|
+
client = Blog.new # Blog::Mock
|
462
|
+
client.data # Cistern::Data::Hash
|
415
463
|
client.data["posts"] += ["x"] # ["x"]
|
416
464
|
```
|
417
465
|
|
@@ -426,7 +474,7 @@ Blog::Mock.data["posts"] # ["x"]
|
|
426
474
|
```ruby
|
427
475
|
client.data.object_id # 70199868585600
|
428
476
|
client.reset!
|
429
|
-
client.data["posts"]
|
477
|
+
client.data["posts"] # []
|
430
478
|
client.data.object_id # 70199868566840
|
431
479
|
```
|
432
480
|
|
data/lib/cistern.rb
CHANGED
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Cistern::Associations
|
3
|
+
|
4
|
+
# Lists the associations defined on the resource
|
5
|
+
# @return [Hash{Symbol=>Array}] mapping of association type to name
|
6
|
+
def associations
|
7
|
+
@associations ||= Hash.new { |h,k| h[k] = [] }
|
8
|
+
end
|
9
|
+
|
10
|
+
# Define an assocation that references a collection.
|
11
|
+
# @param name [Symbol] name of association and corresponding reader and writer.
|
12
|
+
# @param scope [Proc] returning {Cistern::Collection} instance to load models into. {#scope} is evaluated within the
|
13
|
+
# context of the model.
|
14
|
+
# @return [Cistern::Collection] as defined by {#scope}
|
15
|
+
# @example
|
16
|
+
# class Firm < Law::Model
|
17
|
+
# identity :registration_id
|
18
|
+
# has_many :lawyers, -> { cistern.associates(firm_id: identity) }
|
19
|
+
# end
|
20
|
+
def has_many(name, scope)
|
21
|
+
name_sym = name.to_sym
|
22
|
+
|
23
|
+
reader_method = name
|
24
|
+
writer_method = "#{name}="
|
25
|
+
|
26
|
+
attribute name, type: :array
|
27
|
+
|
28
|
+
define_method reader_method do
|
29
|
+
collection = instance_exec(&scope)
|
30
|
+
records = attributes[name_sym] || []
|
31
|
+
|
32
|
+
collection.load(records) if records.any?
|
33
|
+
collection
|
34
|
+
end
|
35
|
+
|
36
|
+
define_method writer_method do |models|
|
37
|
+
attributes[name] = Array(models).map do |model|
|
38
|
+
model.respond_to?(:attributes) ? model.attributes : model
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
associations[:has_many] << name_sym
|
43
|
+
end
|
44
|
+
|
45
|
+
# Define an assocation that references a model.
|
46
|
+
# @param name [Symbol] name of association and corresponding reader.
|
47
|
+
# @param scope [Proc] returning a {Cistern::Model} that is evaluated within the context of the model.
|
48
|
+
# @return [Cistern::Model] as defined by {#scope}
|
49
|
+
# @example
|
50
|
+
# class Firm < Law::Model
|
51
|
+
# identity :registration_id
|
52
|
+
# belongs_to :leader, -> { cistern.employees.get(:ceo) }
|
53
|
+
# end
|
54
|
+
def belongs_to(name, block)
|
55
|
+
name_sym = name.to_sym
|
56
|
+
|
57
|
+
reader_method = name
|
58
|
+
writer_method = "#{name}="
|
59
|
+
|
60
|
+
attribute name_sym
|
61
|
+
|
62
|
+
define_method reader_method do
|
63
|
+
model = instance_exec(&block)
|
64
|
+
attributes[name_sym] = model.attributes
|
65
|
+
model
|
66
|
+
end
|
67
|
+
|
68
|
+
define_method writer_method do |model|
|
69
|
+
data = model.respond_to?(:attributes) ? model.attributes : model
|
70
|
+
attributes[name_sym] = data
|
71
|
+
model
|
72
|
+
end
|
73
|
+
|
74
|
+
associations[:belongs_to] << name_sym
|
75
|
+
end
|
76
|
+
end
|
data/lib/cistern/collection.rb
CHANGED
data/lib/cistern/model.rb
CHANGED
@@ -6,6 +6,7 @@ module Cistern::Model
|
|
6
6
|
klass.send(:extend, Cistern::Attributes::ClassMethods)
|
7
7
|
klass.send(:include, Cistern::Attributes::InstanceMethods)
|
8
8
|
klass.send(:extend, Cistern::Model::ClassMethods)
|
9
|
+
klass.send(:extend, Cistern::Associations)
|
9
10
|
end
|
10
11
|
|
11
12
|
def self.cistern_model(cistern, klass, name)
|
data/lib/cistern/version.rb
CHANGED
@@ -0,0 +1,155 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Cistern::Associations do
|
4
|
+
subject { Class.new(Sample::Model) }
|
5
|
+
|
6
|
+
describe '#associations' do
|
7
|
+
before {
|
8
|
+
Sample::Associate = Class.new(Sample::Model) do
|
9
|
+
identity :id
|
10
|
+
|
11
|
+
belongs_to :association, -> { [] }
|
12
|
+
has_many :associates, -> { [] }
|
13
|
+
end
|
14
|
+
}
|
15
|
+
|
16
|
+
it 'returns a mapping of associations types to names' do
|
17
|
+
expect(Sample::Associate.associations).to eq(
|
18
|
+
{
|
19
|
+
:belongs_to => [:association],
|
20
|
+
:has_many => [:associates],
|
21
|
+
}
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '#belongs_to' do
|
27
|
+
before {
|
28
|
+
Sample::Associate = Class.new(Sample::Model) do
|
29
|
+
identity :id
|
30
|
+
end
|
31
|
+
|
32
|
+
subject.class_eval do
|
33
|
+
identity :id
|
34
|
+
attribute :associate_id
|
35
|
+
|
36
|
+
belongs_to :associate, -> { Sample::Associate.new(id: associate_id) }
|
37
|
+
end
|
38
|
+
}
|
39
|
+
|
40
|
+
it 'returns an associated model' do
|
41
|
+
sample = subject.new(id: 1, associate_id: 2)
|
42
|
+
|
43
|
+
belongs_to = Sample::Associate.new(id: 2)
|
44
|
+
|
45
|
+
expect(sample.associate).to eq(belongs_to)
|
46
|
+
end
|
47
|
+
|
48
|
+
describe '{belongs_to}=' do
|
49
|
+
it 'accepts a model' do
|
50
|
+
sample = subject.new(id: 1, associate_id: 2)
|
51
|
+
|
52
|
+
belongs_to = Sample::Associate.new(id: 2)
|
53
|
+
|
54
|
+
expect(sample.associate).to eq(belongs_to)
|
55
|
+
expect(sample.attributes[:associate]).to eq(id: 2)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'accepts raw data' do
|
59
|
+
sample = subject.new
|
60
|
+
sample.associate = { id: 2 }
|
61
|
+
|
62
|
+
expect(sample.attributes[:associate]).to eq(id: 2)
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'allows for overrides' do
|
66
|
+
subject.class_eval do
|
67
|
+
alias cistern_associate= associate=
|
68
|
+
def associate=(associate)
|
69
|
+
self.cistern_associate = associate
|
70
|
+
self.associate_id = attributes[:associate][:id]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
sample = subject.new(id: 1, associate_id: 3)
|
75
|
+
|
76
|
+
belongs_to = Sample::Associate.new(id: 2)
|
77
|
+
|
78
|
+
expect {
|
79
|
+
sample.associate = belongs_to
|
80
|
+
}.to change(sample, :associate_id).to(2)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe '#has_many' do
|
86
|
+
before {
|
87
|
+
Sample::Associate = Class.new(Sample::Model) do
|
88
|
+
identity :id
|
89
|
+
end
|
90
|
+
|
91
|
+
Sample::Associates = Class.new(Sample::Collection) do
|
92
|
+
|
93
|
+
attribute :associate_id
|
94
|
+
|
95
|
+
model Sample::Associate
|
96
|
+
|
97
|
+
def all
|
98
|
+
load([{id: associate_id + 1}])
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
subject.class_eval do
|
103
|
+
identity :id
|
104
|
+
attribute :associate_id
|
105
|
+
|
106
|
+
has_many :associates, -> { Sample::Associates.new(associate_id: associate_id) }
|
107
|
+
end
|
108
|
+
}
|
109
|
+
|
110
|
+
it 'returns associated models' do
|
111
|
+
expected = Sample::Associates.new(associate_id: 2).load([{id: 3}])
|
112
|
+
|
113
|
+
expect(subject.new(associate_id: 2).associates.all).to eq(expected)
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'does not consider the associated collection loaded without records' do
|
117
|
+
model = subject.new(associate_id: 2)
|
118
|
+
model.associates = []
|
119
|
+
|
120
|
+
expect(model.associates.loaded).to eq(false)
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'considers the associated collection loaded with records' do
|
124
|
+
model = subject.new(associate_id: 2)
|
125
|
+
model.associates = Sample::Associates.new(associate_id: 2).load([{id: 3}])
|
126
|
+
|
127
|
+
expect(model.associates.loaded).to eq(true)
|
128
|
+
expect(model.associates.records).to contain_exactly(Sample::Associate.new(id: 3))
|
129
|
+
end
|
130
|
+
|
131
|
+
describe '{has_many}=' do
|
132
|
+
it 'accepts models' do
|
133
|
+
model = subject.new(associate_id: 2)
|
134
|
+
associates_data = [ { id: 1 }, { id: 2 }]
|
135
|
+
associates = Sample::Associates.new.load(associates_data)
|
136
|
+
|
137
|
+
model.associates = [ Sample::Associate.new(id: 1), Sample::Associate.new(id: 2) ]
|
138
|
+
|
139
|
+
expect(model.attributes[:associates]).to eq(associates_data)
|
140
|
+
expect(model.associates).to eq(associates)
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'accepts raw data' do
|
144
|
+
model = subject.new(associate_id: 2)
|
145
|
+
associates_data = [ { id: 1 }, { id: 2 }]
|
146
|
+
associates = Sample::Associates.new.load(associates_data)
|
147
|
+
|
148
|
+
model.associates = associates_data
|
149
|
+
|
150
|
+
expect(model.attributes[:associates]).to eq(associates_data)
|
151
|
+
expect(model.associates).to eq(associates)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cistern
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josh Lane
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-07-
|
11
|
+
date: 2016-07-26 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: API client framework extracted from Fog
|
14
14
|
email:
|
@@ -31,6 +31,7 @@ files:
|
|
31
31
|
- cistern.gemspec
|
32
32
|
- gemfiles/ruby_lt_2.0.gemfile
|
33
33
|
- lib/cistern.rb
|
34
|
+
- lib/cistern/associations.rb
|
34
35
|
- lib/cistern/attributes.rb
|
35
36
|
- lib/cistern/client.rb
|
36
37
|
- lib/cistern/collection.rb
|
@@ -53,6 +54,7 @@ files:
|
|
53
54
|
- lib/cistern/timeout.rb
|
54
55
|
- lib/cistern/version.rb
|
55
56
|
- lib/cistern/wait_for.rb
|
57
|
+
- spec/associations_spec.rb
|
56
58
|
- spec/attributes_spec.rb
|
57
59
|
- spec/client_spec.rb
|
58
60
|
- spec/collection_spec.rb
|
@@ -93,6 +95,7 @@ signing_key:
|
|
93
95
|
specification_version: 4
|
94
96
|
summary: API client framework
|
95
97
|
test_files:
|
98
|
+
- spec/associations_spec.rb
|
96
99
|
- spec/attributes_spec.rb
|
97
100
|
- spec/client_spec.rb
|
98
101
|
- spec/collection_spec.rb
|