cistern 2.6.0 → 2.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2f58960f3c738abbdd7203d8973981f71c370a65
4
- data.tar.gz: 94a23880c49416884ece0e3615ca7e170bba7234
3
+ metadata.gz: be9990e15cdccbe77ea1c27b5a6961fecd445129
4
+ data.tar.gz: d466f0e83e49c33fc9dce01eb713b9f1062e8f7e
5
5
  SHA512:
6
- metadata.gz: 56fd5780ad10f6c44ff6704743cf927bb50600b3c45a00d1aa1afe8c2d4e237f5413bdc416e282255f924763c3fc19d2bc0afd11c52bcfb7ecf7e2f3e6c39fbd
7
- data.tar.gz: aee0f212eb1923eb92131faa80a5a8fa1c8ae021e0fd1c485fb5b9ac653fba99558d71757d77fdd9baf9558dbdf27d24b72d11c5ce1cd55e52f6c2aee1fbc771
6
+ metadata.gz: dd4a3d7c95873a7fb999854846db84790273ad3366a018dc0e519d7708a13ec325a3a0d1b0bb4968f28cd6260dc4d72659c513513f56dfc2ec0b52ece8a9fb9d
7
+ data.tar.gz: 68ca70761102c30cca381a029184250a1750dc01d3c63626afe8b7ecd1929839497a8c6b55f995cef40ad8d5554b534987e1447f44eae567ae6c805cce82c2b5
@@ -1,8 +1,7 @@
1
1
  # Change Log
2
2
 
3
- ## [Unreleased](https://github.com/lanej/cistern/tree/HEAD)
4
-
5
- [Full Changelog](https://github.com/lanej/cistern/compare/v2.5.0...HEAD)
3
+ ## [v2.6.0](https://github.com/lanej/cistern/tree/v2.6.0) (2016-07-26)
4
+ [Full Changelog](https://github.com/lanej/cistern/compare/v2.5.0...v2.6.0)
6
5
 
7
6
  **Implemented enhancements:**
8
7
 
data/README.md CHANGED
@@ -1,5 +1,6 @@
1
1
  # Cistern
2
2
 
3
+ [![Join the chat at https://gitter.im/lanej/cistern](https://badges.gitter.im/lanej/cistern.svg)](https://gitter.im/lanej/cistern?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
3
4
  [![Build Status](https://secure.travis-ci.org/lanej/cistern.png)](http://travis-ci.org/lanej/cistern)
4
5
  [![Dependencies](https://gemnasium.com/lanej/cistern.png)](https://gemnasium.com/lanej/cistern.png)
5
6
  [![Gem Version](https://badge.fury.io/rb/cistern.svg)](http://badge.fury.io/rb/cistern)
@@ -9,40 +10,11 @@ Cistern helps you consistently build your API clients and faciliates building mo
9
10
 
10
11
  ## Usage
11
12
 
12
- ### Notice: Cistern 3.0
13
+ ### Client
13
14
 
14
- Cistern 3.0 will change the way Cistern interacts with your `Request`, `Collection` and `Model` classes.
15
+ This represents the remote service that you are wrapping. It defines the client's namespace and initialization parameters.
15
16
 
16
- Prior to 3.0, your `Request`, `Collection` and `Model` classes would have inherited from `<service>::Client::Request`, `<service>::Client::Collection` and `<service>::Client::Model` classes, respectively.
17
-
18
- In cistern `~> 3.0`, the default will be for `Request`, `Collection` and `Model` classes to instead include their respective `<service>::Client` modules.
19
-
20
- If you want to be forwards-compatible today, you can configure your client by using `Cistern::Client.with`
21
-
22
- ```ruby
23
- class Blog
24
- include Cistern::Client.with(interface: :module)
25
- end
26
- ```
27
-
28
- Now request classes would look like:
29
-
30
- ```ruby
31
- class Blog::GetPost
32
- include Blog::Request
33
-
34
- def real
35
- "post"
36
- end
37
- end
38
- ```
39
-
40
-
41
- ### Service
42
-
43
- This represents the remote service that you are wrapping. If the service name is `blog` then a good name is `Blog`.
44
-
45
- Service initialization parameters are enumerated by `requires` and `recognizes`. Parameters defined using `recognizes` are optional.
17
+ Client initialization parameters are enumerated by `requires` and `recognizes`. Parameters defined using `recognizes` are optional.
46
18
 
47
19
  ```ruby
48
20
  # lib/blog.rb
@@ -62,8 +34,7 @@ Blog.new(hmac_id: "1", url: "http://example.org")
62
34
  Blog.new(hmac_id: "1")
63
35
  ```
64
36
 
65
- Cistern will define for you two classes, `Mock` and `Real`. Create the corresponding files and initialzers for your
66
- new service.
37
+ Cistern will define for two namespaced classes, `Blog::Mock` and `Blog::Real`. Create the corresponding files and initialzers for your new service.
67
38
 
68
39
  ```ruby
69
40
  # lib/blog/real.rb
@@ -105,74 +76,69 @@ real.is_a?(Blog::Real) # true
105
76
  fake.is_a?(Blog::Mock) # true
106
77
  ```
107
78
 
108
- ### Working with data
79
+ ### Requests
109
80
 
110
- `Cistern::Hash` contains many useful functions for working with data normalization and transformation.
81
+ Requests are defined by subclassing `#{service}::Request`.
111
82
 
112
- **#stringify_keys**
83
+ * `cistern` represents the associated `Blog` instance.
84
+ * `#call` represents the primary entrypoint. Invoked when calling `client#{request_method}`.
85
+ * `#dispatch` determines which method to call. (`#mock` or `#real`)
86
+
87
+ For example:
113
88
 
114
89
  ```ruby
115
- # anywhere
116
- Cistern::Hash.stringify_keys({a: 1, b: 2}) #=> {'a' => 1, 'b' => 2}
117
- # within a Resource
118
- hash_stringify_keys({a: 1, b: 2}) #=> {'a' => 1, 'b' => 2}
119
- ```
90
+ class Blog::UpdatePost
91
+ include Blog::Request
120
92
 
121
- **#slice**
93
+ def real(id, parameters)
94
+ cistern.connection.patch("/post/#{id}", parameters)
95
+ end
122
96
 
123
- ```ruby
124
- # anywhere
125
- Cistern::Hash.slice({a: 1, b: 2, c: 3}, :a, :c) #=> {a: 1, c: 3}
126
- # within a Resource
127
- hash_slice({a: 1, b: 2, c: 3}, :a, :c) #=> {a: 1, c: 3}
128
- ```
97
+ def mock(id, parameters)
98
+ post = cistern.data[:posts].fetch(id)
129
99
 
130
- **#except**
100
+ post.merge!(stringify_keys(parameters))
131
101
 
132
- ```ruby
133
- # anywhere
134
- Cistern::Hash.except({a: 1, b: 2}, :a) #=> {b: 2}
135
- # within a Resource
136
- hash_except({a: 1, b: 2}, :a) #=> {b: 2}
102
+ response(post: post)
103
+ end
104
+ end
137
105
  ```
138
106
 
107
+ However, if you want to add some preprocessing to your request's arguments override `#call` and call `#dispatch`. You
108
+ can also alter the response method's signatures based on the arguments provided to `#dispatch`.
139
109
 
140
- **#except!**
141
110
 
142
111
  ```ruby
143
- # same as #except but modify specified Hash in-place
144
- Cistern::Hash.except!({:a => 1, :b => 2}, :a) #=> {:b => 2}
145
- # within a Resource
146
- hash_except!({:a => 1, :b => 2}, :a) #=> {:b => 2}
147
- ```
112
+ class Blog::UpdatePost
113
+ include Blog::Request
148
114
 
115
+ attr_reader :parameters
149
116
 
150
- ### Requests
117
+ def call(post_id, parameters)
118
+ @parameters = stringify_keys(parameters)
119
+ dispatch(Integer(post_id))
120
+ end
151
121
 
152
- Requests are defined by subclassing `#{service}::Request`.
122
+ def real(id)
123
+ cistern.connection.patch("/post/#{id}", parameters)
124
+ end
153
125
 
154
- * `cistern` represents the associated `Blog` instance.
126
+ def mock(id)
127
+ post = cistern.data[:posts].fetch(id)
155
128
 
156
- ```ruby
157
- class Blog::GetPost < Blog::Request
158
- def real(params)
159
- # make a real request
160
- "i'm real"
161
- end
129
+ post.merge!(parameters)
162
130
 
163
- def mock(params)
164
- # return a fake response
165
- "imposter!"
131
+ response(post: post)
166
132
  end
167
133
  end
168
-
169
- Blog.new.get_post # "i'm real"
170
134
  ```
171
135
 
172
136
  The `#cistern_method` function allows you to specify the name of the generated method.
173
137
 
174
138
  ```ruby
175
- class Blog::GetPosts < Blog::Request
139
+ class Blog::GetPosts
140
+ include Blog::Request
141
+
176
142
  cistern_method :get_all_the_posts
177
143
 
178
144
  def real(params)
@@ -264,7 +230,8 @@ Cistern attributes are designed to make your model flexible and developer friend
264
230
  For example:
265
231
 
266
232
  ```ruby
267
- class Blog::Post < Blog::Model
233
+ class Blog::Post
234
+ include Blog::Model
268
235
  identity :id, type: :integer
269
236
 
270
237
  attribute :body
@@ -372,7 +339,8 @@ post.data.views #=> 3
372
339
  * `load` consumes an Array of data and constructs matching `model` instances
373
340
 
374
341
  ```ruby
375
- class Blog::Posts < Blog::Collection
342
+ class Blog::Posts
343
+ include Blog::Collection
376
344
 
377
345
  attribute :count, type: :integer
378
346
 
@@ -415,7 +383,9 @@ There are two types of associations available.
415
383
  * `has_many` references a collection of resources and defines a reader / writer.
416
384
 
417
385
  ```ruby
418
- class Blog::Tag < Blog::Model
386
+ class Blog::Tag
387
+ include Blog::Model
388
+
419
389
  identity :id
420
390
  attribute :author_id
421
391
 
@@ -435,7 +405,8 @@ tag.creator = blogs.author.get(name: 'phil')
435
405
  tag.attributes[:creator] #=> { 'id' => 2, 'name' => 'phil' }
436
406
  ```
437
407
 
438
- Foreign keys can be updated with association writing by overwriting the writer.
408
+ Foreign keys can be updated with with the association writer by aliasing the original writer and accessing the
409
+ underlying attributes.
439
410
 
440
411
  ```ruby
441
412
  Blog::Tag.class_eval do
@@ -505,6 +476,48 @@ class Blog
505
476
  end
506
477
  ```
507
478
 
479
+ ### Working with data
480
+
481
+ `Cistern::Hash` contains many useful functions for working with data normalization and transformation.
482
+
483
+ **#stringify_keys**
484
+
485
+ ```ruby
486
+ # anywhere
487
+ Cistern::Hash.stringify_keys({a: 1, b: 2}) #=> {'a' => 1, 'b' => 2}
488
+ # within a Resource
489
+ hash_stringify_keys({a: 1, b: 2}) #=> {'a' => 1, 'b' => 2}
490
+ ```
491
+
492
+ **#slice**
493
+
494
+ ```ruby
495
+ # anywhere
496
+ Cistern::Hash.slice({a: 1, b: 2, c: 3}, :a, :c) #=> {a: 1, c: 3}
497
+ # within a Resource
498
+ hash_slice({a: 1, b: 2, c: 3}, :a, :c) #=> {a: 1, c: 3}
499
+ ```
500
+
501
+ **#except**
502
+
503
+ ```ruby
504
+ # anywhere
505
+ Cistern::Hash.except({a: 1, b: 2}, :a) #=> {b: 2}
506
+ # within a Resource
507
+ hash_except({a: 1, b: 2}, :a) #=> {b: 2}
508
+ ```
509
+
510
+
511
+ **#except!**
512
+
513
+ ```ruby
514
+ # same as #except but modify specified Hash in-place
515
+ Cistern::Hash.except!({:a => 1, :b => 2}, :a) #=> {:b => 2}
516
+ # within a Resource
517
+ hash_except!({:a => 1, :b => 2}, :a) #=> {:b => 2}
518
+ ```
519
+
520
+
508
521
  #### Storage
509
522
 
510
523
  Currently supported storage backends are:
@@ -588,10 +601,75 @@ class Blog::GetPost
588
601
  end
589
602
  ```
590
603
 
604
+ ## ~> 3.0
605
+
606
+ ### Request Dispatch
607
+
608
+ Default request interface passes through `#_mock` and `#_real` depending on the client mode.
609
+
610
+ ```ruby
611
+ class Blog::GetPost
612
+ include Blog::Request
613
+
614
+ def setup(post_id, parameters)
615
+ [post_id, stringify_keys(parameters)]
616
+ end
617
+
618
+ def _mock(*args)
619
+ mock(*setup(*args))
620
+ end
621
+
622
+ def _real(post_id, parameters)
623
+ real(*setup(*args))
624
+ end
625
+ end
626
+ ```
627
+
628
+ In cistern 3, requests pass through `#call` in both modes. `#dispatch` is responsible for determining the mode and
629
+ calling the appropriate method.
630
+
631
+ ```ruby
632
+ class Blog::GetPost
633
+ include Blog::Request
634
+
635
+ def call(post_id, parameters)
636
+ normalized_parameters = stringify_keys(parameters)
637
+ dispatch(post_id, normalized_parameters)
638
+ end
639
+ end
640
+ ```
641
+
642
+ ### Client definition
643
+
644
+ Default resource definition is done by inheritance.
645
+
646
+ ```ruby
647
+ class Blog::Post < Blog::Model
648
+ end
649
+ ```
650
+
651
+ In cistern 3, resource definition is done by module inclusion.
652
+
653
+ ```ruby
654
+ class Blog::Post
655
+ include Blog::Post
656
+ end
657
+ ```
658
+
659
+ Prepare for cistern 3 by using `Cistern::Client.with(interface: :module)` when defining the client.
660
+
661
+ ```ruby
662
+ class Blog
663
+ include Cistern::Client.with(interface: :module)
664
+ end
665
+ ```
666
+
591
667
  ## Examples
592
668
 
593
669
  * [zendesk2](https://github.com/lanej/zendesk2)
594
670
  * [you_track](https://github.com/lanej/you_track)
671
+ * [ey-core](https://github.com/engineyard/core-client-rb)
672
+
595
673
 
596
674
  ## Releasing
597
675
 
@@ -44,7 +44,7 @@ module Cistern::Client
44
44
 
45
45
  if interface == :class
46
46
  Cistern.deprecation(
47
- %q{class' interface is deprecated. Use `include Cistern::Client.with(interface: :module). See https://github.com/lanej/cistern#custom-architecture},
47
+ %q{'class' interface is deprecated. Use `include Cistern::Client.with(interface: :module). See https://github.com/lanej/cistern#custom-architecture},
48
48
  caller[2],
49
49
  )
50
50
  end
@@ -85,11 +85,19 @@ module Cistern::Client
85
85
  class Real
86
86
  def initialize(options={})
87
87
  end
88
+
89
+ def mocking?
90
+ false
91
+ end
88
92
  end
89
93
 
90
94
  class Mock
91
95
  def initialize(options={})
92
96
  end
97
+
98
+ def mocking?
99
+ true
100
+ end
93
101
  end
94
102
 
95
103
  #{interface} #{model_class}
@@ -184,14 +192,6 @@ module Cistern::Client
184
192
 
185
193
  super
186
194
  end
187
-
188
- def _mock(*args)
189
- mock(*args)
190
- end
191
-
192
- def _real(*args)
193
- real(*args)
194
- end
195
195
  end
196
196
  EOS
197
197
 
@@ -1,22 +1,35 @@
1
1
  module Cistern::Request
2
2
  include Cistern::HashSupport
3
3
 
4
+ module ClassMethods
5
+ # @deprecated Use {#cistern_method} instead
6
+ def service_method(name = nil)
7
+ Cistern.deprecation(
8
+ '#service_method is deprecated. Please use #cistern_method',
9
+ caller[0]
10
+ )
11
+ @_cistern_method ||= name
12
+ end
13
+
14
+ def cistern_method(name = nil)
15
+ @_cistern_method ||= name
16
+ end
17
+ end
18
+
4
19
  def self.cistern_request(cistern, klass, name)
5
20
  unless klass.name || klass.cistern_method
6
21
  fail ArgumentError, "can't turn anonymous class into a Cistern request"
7
22
  end
8
23
 
9
- cistern::Mock.module_eval <<-EOS, __FILE__, __LINE__
24
+ method = <<-EOS
10
25
  def #{name}(*args)
11
- #{klass}.new(self)._mock(*args)
26
+ #{klass}.new(self).call(*args)
12
27
  end
13
28
  EOS
14
29
 
15
- cistern::Real.module_eval <<-EOS, __FILE__, __LINE__
16
- def #{name}(*args)
17
- #{klass}.new(self)._real(*args)
18
- end
19
- EOS
30
+
31
+ cistern::Mock.module_eval method, __FILE__, __LINE__
32
+ cistern::Real.module_eval method, __FILE__, __LINE__
20
33
  end
21
34
 
22
35
  def self.service_request(*args)
@@ -41,18 +54,35 @@ module Cistern::Request
41
54
  @cistern = cistern
42
55
  end
43
56
 
44
- module ClassMethods
45
- # @deprecated Use {#cistern_method} instead
46
- def service_method(name = nil)
57
+ def call(*args)
58
+ dispatch(*args)
59
+ end
60
+
61
+ def real(*)
62
+ raise NotImplementedError
63
+ end
64
+
65
+ def mock(*)
66
+ raise NotImplementedError
67
+ end
68
+
69
+ protected
70
+
71
+ # @fixme remove _{mock,real} methods and call {mock,real} directly before 3.0 release.
72
+ def dispatch(*args)
73
+ to = cistern.mocking? ? :mock : :real
74
+
75
+ legacy_method = :"_#{to}"
76
+
77
+ if respond_to?(legacy_method)
47
78
  Cistern.deprecation(
48
- '#service_method is deprecated. Please use #cistern_method',
79
+ '#_mock is deprecated. Please use #mock and/or #call. See https://github.com/lanej/cistern#request-dispatch',
49
80
  caller[0]
50
81
  )
51
- @_cistern_method ||= name
52
- end
53
82
 
54
- def cistern_method(name = nil)
55
- @_cistern_method ||= name
83
+ public_send(legacy_method, *args)
84
+ else
85
+ public_send(to, *args)
56
86
  end
57
87
  end
58
88
  end
@@ -15,6 +15,7 @@ module Cistern::Singular
15
15
  klass.send(:extend, Cistern::Attributes::ClassMethods)
16
16
  klass.send(:include, Cistern::Attributes::InstanceMethods)
17
17
  klass.send(:extend, Cistern::Model::ClassMethods)
18
+ klass.send(:extend, Cistern::Associations)
18
19
  end
19
20
 
20
21
  def collection
@@ -1,3 +1,3 @@
1
1
  module Cistern
2
- VERSION = '2.6.0'
2
+ VERSION = '2.7.0'
3
3
  end
@@ -1,57 +1,79 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe 'Cistern::Request' do
4
- class RequestService
5
- include Cistern::Client
6
-
7
- recognizes :key
4
+ before {
5
+ Sample.class_eval do
6
+ recognizes :key
7
+ end
8
8
 
9
- class Real
9
+ Sample::Real.class_eval do
10
10
  attr_reader :service_args
11
11
 
12
12
  def initialize(*args)
13
13
  @service_args = args
14
14
  end
15
15
  end
16
- end
16
+ }
17
17
 
18
- # @todo Sample::Service.request
19
- class ListSamples < RequestService::Request
20
- cistern_method :list_all_samples
18
+ describe '#cistern_method' do
19
+ it 'remaps the client request method' do
20
+ class ListSamples < Sample::Request
21
+ cistern_method :list_all_samples
22
+ end
21
23
 
22
- def real(*args)
23
- cistern.service_args + args + ['real']
24
+ expect(Sample.new).to respond_to(:list_all_samples)
25
+ expect(Sample.new).not_to respond_to(:list_samples)
24
26
  end
27
+ end
25
28
 
26
- def mock(*args)
27
- args + ['mock']
29
+ it 'calls the appropriate method' do
30
+ class GetSamples < Sample::Request
31
+ def real(*args)
32
+ cistern.service_args + args + ['real']
33
+ end
34
+
35
+ def mock(*args)
36
+ args + ['mock']
37
+ end
28
38
  end
29
- end
30
39
 
31
- it 'should execute a new-style request' do
32
- expect(RequestService.new.list_all_samples('sample1')).to eq([{}, 'sample1', 'real'])
33
- expect(RequestService::Real.new.list_all_samples('sample2')).to eq(%w(sample2 real))
34
- expect(RequestService::Mock.new.list_all_samples('sample3')).to eq(%w(sample3 mock))
40
+ expect(Sample.new.get_samples('sample1')).to eq([{}, 'sample1', 'real'])
41
+ expect(Sample::Real.new.get_samples('sample2')).to eq(%w(sample2 real))
42
+ expect(Sample::Mock.new.get_samples('sample3')).to eq(%w(sample3 mock))
35
43
 
36
44
  # service access
37
- expect(RequestService.new(key: 'value').list_all_samples('stat')).to eq([{ key: 'value' }, 'stat', 'real'])
45
+ expect(Sample.new(key: 'value').get_samples('stat')).to eq([{ key: 'value' }, 'stat', 'real'])
38
46
  end
39
47
 
40
48
  describe 'deprecation', :deprecated do
41
- class DeprecatedRequestService
42
- include Cistern::Client
49
+ it 'calls _mock and _real if present' do
50
+ class Sample::ListDeprecations < Sample::Request
51
+ def _mock
52
+ :_mock
53
+ end
54
+
55
+ def real
56
+ :real
57
+ end
58
+ end
59
+
60
+ actual = Sample.new.list_deprecations
61
+ expect(actual).to eq(:real)
62
+
63
+ Sample.mock!
64
+
65
+ actual = Sample.new.list_deprecations
66
+ expect(actual).to eq(:_mock)
43
67
  end
44
68
 
45
69
  it 'responds to #service' do
46
- class ListDeprecations < DeprecatedRequestService::Request
47
- service_method :list_deprecations
48
-
70
+ class Sample::ListDeprecations < Sample::Request
49
71
  def real
50
72
  self
51
73
  end
52
74
  end
53
75
 
54
- sample = DeprecatedRequestService.new.list_deprecations
76
+ sample = Sample.new.list_deprecations
55
77
  expect(sample.service).to eq(sample.cistern)
56
78
  end
57
79
  end
@@ -6,6 +6,8 @@ describe 'Cistern::Singular' do
6
6
  attribute :name, type: :string
7
7
  attribute :count, type: :number
8
8
 
9
+ belongs_to :entity, -> { cistern.settings(name: '1') }
10
+
9
11
  def save
10
12
  result = @@settings = attributes.merge(dirty_attributes)
11
13
 
@@ -32,6 +34,10 @@ describe 'Cistern::Singular' do
32
34
  end
33
35
  end
34
36
 
37
+ it 'allows associations' do
38
+ expect(service.settings.load.entity.name).to eq('1')
39
+ end
40
+
35
41
  it 'reloads' do
36
42
  singular = service.settings(count: 0)
37
43
 
@@ -16,6 +16,7 @@ RSpec.configure do |rspec|
16
16
  else
17
17
  rspec.filter_run_excluding(:coverage)
18
18
  end
19
+
19
20
  rspec.around(:each, :deprecated) do |example|
20
21
  original_value = Cistern.deprecation_warnings?
21
22
  Cistern.deprecation_warnings = false
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.6.0
4
+ version: 2.7.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-26 00:00:00.000000000 Z
11
+ date: 2016-08-10 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: API client framework extracted from Fog
14
14
  email: