cistern 2.6.0 → 2.7.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: 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: