cistern 2.5.0 → 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 51b4a1fc74578d0a0a8f89bb9b041501ab850ac9
4
- data.tar.gz: 3f24e1b8145497a84d6f6b0fa449aa11b7d5e7f1
3
+ metadata.gz: 2f58960f3c738abbdd7203d8973981f71c370a65
4
+ data.tar.gz: 94a23880c49416884ece0e3615ca7e170bba7234
5
5
  SHA512:
6
- metadata.gz: 2f7a72df05bb6c95d5176a36b8e8d0d17f9c9bd3179cec7424e4fc66e216e24f148bff977e4ba19f1bcee0d5afad60757e193a83bf926401a9d40be70e75a223
7
- data.tar.gz: f2221f974e0817e5410e88a8d4586f28881756fcbea1836e52d8440ec97f50dfe689ae3758820e1225c2de006bfcfb0dbc0065ab70cbc0980fc2fda2f8a96b78
6
+ metadata.gz: 56fd5780ad10f6c44ff6704743cf927bb50600b3c45a00d1aa1afe8c2d4e237f5413bdc416e282255f924763c3fc19d2bc0afd11c52bcfb7ecf7e2f3e6c39fbd
7
+ data.tar.gz: aee0f212eb1923eb92131faa80a5a8fa1c8ae021e0fd1c485fb5b9ac653fba99558d71757d77fdd9baf9558dbdf27d24b72d11c5ce1cd55e52f6c2aee1fbc771
@@ -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.4.1...HEAD)
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 # Blog::Mock
414
- client.data # Cistern::Data::Hash
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
 
@@ -13,6 +13,7 @@ module Cistern
13
13
  require 'cistern/hash_support'
14
14
  require 'cistern/string'
15
15
  require 'cistern/mock'
16
+ require 'cistern/associations'
16
17
  require 'cistern/wait_for'
17
18
  require 'cistern/attributes'
18
19
  require 'cistern/collection'
@@ -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
@@ -55,6 +55,7 @@ module Cistern::Collection
55
55
  alias_method :build, :initialize
56
56
 
57
57
  def initialize(attributes = {})
58
+ @loaded = false
58
59
  merge_attributes(attributes)
59
60
  end
60
61
 
@@ -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)
@@ -1,3 +1,3 @@
1
1
  module Cistern
2
- VERSION = '2.5.0'
2
+ VERSION = '2.6.0'
3
3
  end
@@ -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.5.0
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-19 00:00:00.000000000 Z
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