acfs 0.5.1 → 0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7b8b60ec789ab339c208b7b78b4bcdf38fb18467
4
- data.tar.gz: cd3c88aec232d7b8e0716b2f10b53eea5741c47d
3
+ metadata.gz: 36f33bb4ccf82d17ac10c0c1a142476817229231
4
+ data.tar.gz: a881457d71f8437c9f81466e928eb4e7cb57df90
5
5
  SHA512:
6
- metadata.gz: ef3105d463581ec7712d3112fcd68fd89f2a82c4389519ba299c4383d2b47454857a5ffc6613bb366367bff8a376bb6b7754b5baaf6ee24cc695ea03166813cd
7
- data.tar.gz: 5e6afdf759f89a7f2d69fa8f5950fc6d99ec2b05648f33302306fc67f022154b79f7d3071b2f2d10880dc187c1aa2fba5c28dff61cf77cca3ee8ef18b86667a4
6
+ metadata.gz: fcb651f2c3c488d05fdebcd2a63ac2c6612a5d19188519456a1d690cccfe6fdc7786aeeddef7ae6dd30170aa9cc11a4b342037d2102ed4eb67d1b983950e8922
7
+ data.tar.gz: eed4cc6c2c6dd9071c40c22fc5786541e7c5e615e708affe2d772e5d7a6e2c9005cb2d97e664bef9acad7cf222fa16904282797a7da1b27c4255a586c65f1aeb
data/README.md CHANGED
@@ -10,10 +10,10 @@ TODO: Write a gem description
10
10
 
11
11
  Add this line to your application's Gemfile:
12
12
 
13
- gem 'acfs', '0.3.0'
13
+ gem 'acfs', '0.6.0'
14
14
 
15
- **Note:** Acfs is under development. API may change at any time. No semantic versioning will be applied until version
16
- `1.0`. Version `1.0` does not mean a complete feature set but a first stable code base.
15
+ **Note:** Acfs is under development. I'll try to avoid changes to the public
16
+ API but internal APIs may change quite often.
17
17
 
18
18
  And then execute:
19
19
 
@@ -25,71 +25,113 @@ Or install it yourself as:
25
25
 
26
26
  ## Usage
27
27
 
28
- ### Attributes
28
+ First you need to define your service(s):
29
29
 
30
30
  ```ruby
31
- class MyModel
32
- include Acfs::Model
33
-
34
- attribute :name, :string
35
- attribute :age, :integer, default: 15
31
+ class UserService < Acfs::Service
32
+ self.base_url = 'http://users.myapp.org'
36
33
  end
37
-
38
- MyModel.attributes # => { "name" => "", "age" => 15 }
39
-
40
- mo = MyModel.new name: 'Johnny', age: 12
41
- mo.name # => "Johnny"
42
- mo.age = '13'
43
- mo.age # => 13
44
- mo.attributes # => { "name" => "Johnny", "age" => 13 }
45
34
  ```
46
35
 
47
- ### Service, Model & Collection
36
+ This specifies where the `UserService` can be reached. You can now create some
37
+ models representing resources serviced by the `UserService`.
48
38
 
49
39
  ```ruby
50
- class MyService < Acfs::Service
51
- self.base_url = 'http://acc.srv'
52
- end
53
-
54
40
  class User
55
41
  include Acfs::Model
56
- service MyService
42
+ service UserService # Associate `User` model with `UserService`.
43
+
44
+ # Define model attributes and types
45
+ # Types are needed to parse and generate request and response payload.
46
+
47
+ attribute :id, :uuid # Types can be classes or symbols.
48
+ # Symbols will be used to load a class from `Acfs::Model::Attributes` namespace.
49
+ # Eg. `:uuid` will load class `Acfs::Model::Attributes::Uuid`.
50
+
51
+ attribute :name, :string, default: 'Anonymous'
52
+ attribute :age, ::Acfs::Model::Attributes::Integer # Or use :integer
57
53
 
58
- attribute :id, :integer
59
54
  end
55
+ ```
60
56
 
57
+ The service and model classes can be shipped as a gem or git submodule to be
58
+ included by the frontend application(s).
59
+
60
+ You can use the model there:
61
+
62
+ ```ruby
61
63
  @user = User.find 14
62
64
 
63
65
  @user.loaded? #=> false
64
66
 
65
67
  Acfs.run # This will run all queued request as parallel as possible.
66
68
  # For @user the following URL will be requested:
67
- # `http://acc.srv/users/14`
69
+ # `http://users.myapp.org/users/14`
68
70
 
69
71
  @model.name # => "..."
70
72
 
71
73
  @users = User.all
72
74
  @users.loaded? #=> false
73
75
 
74
- Acfs.run # Will request `http://acc.srv/users`
76
+ Acfs.run # Will request `http://users.myapp.org/users`
75
77
 
76
78
  @users #=> [<User>, ...]
77
79
  ```
78
80
 
81
+ If you need multiple resources or dependent resources first define a "plan"
82
+ how they can be loaded:
83
+
84
+ ```ruby
85
+ @user = User.find(5) do |user|
86
+ # Block will be executed right after user with id 5 is loaded
87
+
88
+ # You can load additional resources also from other services
89
+ # Eg. fetch comments from `CommentSerivce`. The line below will
90
+ # load comments from `http://comments.myapp.org/comments?user=5`
91
+ @comments = Comment.where user: user.id
92
+
93
+ # You can load multiple resources in parallel if you have multiple
94
+ # ids.
95
+ @friends = User.find 1, 4, 10 do |friends|
96
+ # This block will be executed when all friends are loaded.
97
+ # [ ... ]
98
+ end
99
+ end
100
+
101
+ Acfs.run # This call will fire all request as parallel as possible.
102
+ # The sequence above would look similar to:
103
+ #
104
+ # Start Fin
105
+ # |===================| `Acfs.run`
106
+ # |====| /users/5
107
+ # | |==============| /comments?user=5
108
+ # | |======| /users/1
109
+ # | |=======| /users/4
110
+ # | |======| /users/10
111
+
112
+ # Now we can access all resources:
113
+
114
+ @user.name # => "John
115
+ @comments.size # => 25
116
+ @friends[0].name # => "Miraculix"
117
+
79
118
  ## TODO
80
119
 
81
- * Develop Library
120
+ * Create/Update operations
121
+ * High level features
122
+ ** Pagination? Filtering? (If service API provides such features.)
123
+ ** Messaging Queue support for services and models
82
124
  * Documentation
83
125
 
84
126
  ## Contributing
85
127
 
86
128
  1. Fork it
87
129
  2. Create your feature branch (`git checkout -b my-new-feature`)
88
- 3. Add specs for your feature
89
- 4. Implement your feature
90
- 5. Commit your changes (`git commit -am 'Add some feature'`)
91
- 6. Push to the branch (`git push origin my-new-feature`)
92
- 7. Create new Pull Request
130
+ 3a. Add specs for your feature
131
+ 3b. Implement your feature
132
+ 3c. Commit your changes (`git commit -am 'Add some feature'`)
133
+ 4. Push to the branch (`git push origin my-new-feature`)
134
+ 5. Create new Pull Request
93
135
 
94
136
  ## License
95
137
 
data/acfs.gemspec CHANGED
@@ -30,4 +30,5 @@ Gem::Specification.new do |spec|
30
30
  spec.add_development_dependency 'rspec'
31
31
  spec.add_development_dependency 'guard-rspec'
32
32
  spec.add_development_dependency 'coveralls'
33
+ spec.add_development_dependency 'msgpack'
33
34
  end
data/lib/acfs.rb CHANGED
@@ -17,6 +17,7 @@ module Acfs
17
17
  autoload :Base
18
18
  autoload :Print
19
19
  autoload :JsonDecoder
20
+ autoload :MessagePackDecoder, 'acfs/middleware/msgpack_decoder'
20
21
  end
21
22
 
22
23
  module Adapter
@@ -33,9 +34,9 @@ module Acfs
33
34
  end
34
35
 
35
36
  def queue(req, &block)
36
- request = middleware.call Request.new(req)
37
+ request = Request.new req
37
38
  request.on_complete &block if block_given?
38
- adapter.queue request
39
+ middleware.call request
39
40
  end
40
41
 
41
42
  def adapter
@@ -43,7 +44,9 @@ module Acfs
43
44
  end
44
45
 
45
46
  def middleware
46
- @middleware ||= lambda { |request| request }
47
+ @middleware ||= proc do |request|
48
+ adapter.queue request
49
+ end
47
50
  end
48
51
 
49
52
  def use(klass, options = {})
@@ -54,5 +57,11 @@ module Acfs
54
57
  @middlewares << klass
55
58
  @middleware = klass.new(middleware, options)
56
59
  end
60
+
61
+ def clear
62
+ @middleware = nil
63
+ @middlewares = nil
64
+ end
57
65
  end
58
66
  end
67
+
@@ -13,7 +13,7 @@ module Acfs
13
13
  end
14
14
 
15
15
  def call(request)
16
- request.on_complete { |res| response(res) } if respond_to? :response
16
+ request.on_complete { |res, nxt| response(res, nxt) } if respond_to? :response
17
17
  app.call(request)
18
18
  end
19
19
  end
@@ -7,8 +7,9 @@ module Acfs
7
7
  #
8
8
  class JsonDecoder < Base
9
9
 
10
- def response(response)
10
+ def response(response, nxt)
11
11
  response.data = ::MultiJson.load(response.body) if response.json?
12
+ nxt.call response
12
13
  end
13
14
  end
14
15
  end
@@ -0,0 +1,26 @@
1
+ require 'msgpack'
2
+ require 'action_dispatch'
3
+
4
+ module Acfs
5
+ module Middleware
6
+
7
+ # Register msgpack mime type
8
+ ::Mime::Type.register 'application/x-msgpack', :msgpack
9
+
10
+ # A middleware to decode Message Pack responses.
11
+ #
12
+ class MessagePackDecoder < Base
13
+
14
+ CONTENT_TYPES = %w(application/x-msgpack)
15
+
16
+ def response(response, nxt)
17
+ response.data = ::MessagePack.load(response.body) if message_pack?(response)
18
+ nxt.call response
19
+ end
20
+
21
+ def message_pack?(response)
22
+ CONTENT_TYPES.include? response.content_type
23
+ end
24
+ end
25
+ end
26
+ end
@@ -21,25 +21,15 @@ module Acfs::Model
21
21
  # Try to load a resource by given id.
22
22
  #
23
23
  # Example
24
- # User.find(5) # Will query `http://base.url/users/5`
24
+ # User.find(5) # Will query `http://base.url/users/5`
25
+ # User.find(1, 2, 5) # Will return collection and will query
26
+ # # `http://base.url/users/1`, `http://base.url/users/2`
27
+ # # and `http://base.url/users/5` parallel
25
28
  #
26
- def find(id, options = {}, &block)
27
- model = self.new
28
-
29
- request = case id
30
- when Hash
31
- Acfs::Request.new url, params: id
32
- else
33
- Acfs::Request.new url(id.to_s)
34
- end
29
+ def find(*attrs, &block)
30
+ opts = attrs.extract_options!
35
31
 
36
- service.queue(request) do |response|
37
- model.attributes = response.data
38
- model.loaded!
39
- block.call model unless block.nil?
40
- end
41
-
42
- model
32
+ attrs.size > 1 ? find_multiple(attrs, opts, &block) : find_single(attrs[0], opts, &block)
43
33
  end
44
34
 
45
35
  # Try to load all resources.
@@ -61,6 +51,35 @@ module Acfs::Model
61
51
  collection
62
52
  end
63
53
  alias :where :all
54
+
55
+ private
56
+ def find_single(id, opts, &block)
57
+ model = self.new
58
+
59
+ request = Acfs::Request.new url(id.to_s)
60
+ service.queue(request) do |response|
61
+ model.attributes = response.data
62
+ model.loaded!
63
+ block.call model unless block.nil?
64
+ end
65
+
66
+ model
67
+ end
68
+
69
+ def find_multiple(ids, opts, &block)
70
+ ::Acfs::Collection.new.tap do |collection|
71
+ counter = 0
72
+ ids.each do |id|
73
+ find_single id, opts do |resource|
74
+ collection << resource
75
+ if (counter += 1) == ids.size
76
+ collection.loaded!
77
+ block.call collection unless block.nil?
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
64
83
  end
65
84
  end
66
85
  end
data/lib/acfs/request.rb CHANGED
@@ -2,6 +2,9 @@ require 'acfs/request/callbacks'
2
2
 
3
3
  module Acfs
4
4
 
5
+ # Encapsulate all data required to make up a request to the
6
+ # underlaying http library.
7
+ #
5
8
  class Request
6
9
  attr_accessor :body, :format
7
10
  attr_reader :url, :headers, :params, :data
@@ -21,7 +21,7 @@ module Acfs
21
21
  # @return [ Acfs::Request ] The request itself.
22
22
  #
23
23
  def on_complete(&block)
24
- callbacks << block if block_given?
24
+ callbacks.insert 0, block if block_given?
25
25
  self
26
26
  end
27
27
 
@@ -38,9 +38,14 @@ module Acfs
38
38
  # @return [ Acfs::Request ] The request itself.
39
39
  #
40
40
  def complete!(response)
41
- callbacks.each { |cb| cb.call response }
41
+ call_callback response, 0
42
42
  self
43
43
  end
44
+
45
+ private
46
+ def call_callback(res, index)
47
+ callbacks[index].call res, proc { |res| call_callback res, index + 1 } if index < callbacks.size
48
+ end
44
49
  end
45
50
  end
46
51
  end
@@ -6,15 +6,15 @@ module Acfs
6
6
  # Quick accessors for format handling.
7
7
  module Formats
8
8
 
9
- def mime_type
10
- @mime_type ||= begin
9
+ def content_type
10
+ @content_type ||= begin
11
11
  content_type = headers['Content-Type'].split(/;\s*\w+="?\w+"?/).first
12
12
  Mime::Type.parse(content_type).first
13
13
  end
14
14
  end
15
15
 
16
16
  def json?
17
- mime_type == Mime::JSON
17
+ content_type == Mime::JSON
18
18
  end
19
19
  end
20
20
  end
data/lib/acfs/version.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  module Acfs
2
2
  module VERSION
3
3
  MAJOR = 0
4
- MINOR = 5
5
- PATCH = 1
4
+ MINOR = 6
5
+ PATCH = 0
6
6
  STAGE = nil
7
7
 
8
8
  STRING = [MAJOR, MINOR, PATCH, STAGE].reject(&:nil?).join('.')
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ describe Acfs::Middleware::MessagePackDecoder do
4
+ let(:data) { [{id: 1, name: "Anon"},{id: 2, name:"John", friends: [ 1 ]}] }
5
+ let(:body) { data.to_param }
6
+ let(:headers) { {} }
7
+ let(:request) { Acfs::Request.new "fubar" }
8
+ let(:response) { Acfs::Response.new request, 200, headers, body }
9
+ let(:decoder) { Acfs::Middleware::MessagePackDecoder.new lambda { |req| req } }
10
+
11
+ before do
12
+ decoder.call request
13
+ end
14
+
15
+ context 'with Message Pack response' do
16
+ let(:headers) { { 'Content-Type' => 'application/x-msgpack' } }
17
+ let(:body) { MessagePack.dump data }
18
+
19
+ it 'should decode body data' do
20
+ request.complete! response
21
+
22
+ expect(response.data).to be == data.map(&:stringify_keys)
23
+ end
24
+ end
25
+
26
+ context 'with invalid response' do
27
+ let(:headers) { { 'Content-Type' => 'application/x-msgpack' } }
28
+ let(:body) { MessagePack.dump(data)[4..-4] }
29
+
30
+ it 'should raise an error' do
31
+ expect { request.complete! response }.to raise_error(MessagePack::MalformedFormatError)
32
+ end
33
+ end
34
+
35
+ context 'without Message Pack response' do
36
+ let(:headers) { { 'Content-Type' => 'application/text' } }
37
+ let(:body) { data.to_json }
38
+
39
+ it 'should not decode non-MessagePack encoded responses' do
40
+ request.complete! response
41
+
42
+ expect(response.data).to be_nil
43
+ end
44
+ end
45
+ end
@@ -17,7 +17,7 @@ describe Acfs::Request::Callbacks do
17
17
  request.on_complete &callback
18
18
 
19
19
  expect(request.callbacks).to have(2).item
20
- expect(request.callbacks[1]).to be == callback
20
+ expect(request.callbacks[0]).to be == callback
21
21
  end
22
22
  end
23
23
 
@@ -25,10 +25,22 @@ describe Acfs::Request::Callbacks do
25
25
  let(:response) { Acfs::Response.new(request) }
26
26
 
27
27
  it 'should trigger registered callbacks with given response' do
28
- callback.should_receive(:call).with(response)
28
+ callback.should_receive(:call).with(response, kind_of(Proc))
29
29
 
30
30
  request.on_complete &callback
31
31
  request.complete! response
32
32
  end
33
+
34
+ it 'should trigger multiple callback in reverted insertion order' do
35
+ check = []
36
+
37
+ request.on_complete { |res, nxt| check << 1; nxt.call res }
38
+ request.on_complete { |res, nxt| check << 2; nxt.call res }
39
+ request.on_complete { |res, nxt| check << 3; nxt.call res }
40
+
41
+ request.complete! response
42
+
43
+ expect(check).to be == [3, 2, 1]
44
+ end
33
45
  end
34
46
  end
@@ -11,9 +11,9 @@ describe Acfs::Response::Formats do
11
11
  context 'with JSON mimetype' do
12
12
  let(:mime_type) { 'application/json' }
13
13
 
14
- describe '#mime_type' do
14
+ describe '#content_type' do
15
15
  it 'should return Mime::JSON' do
16
- expect(response.mime_type).to be == Mime::JSON
16
+ expect(response.content_type).to be == Mime::JSON
17
17
  end
18
18
  end
19
19
 
@@ -26,9 +26,9 @@ describe Acfs::Response::Formats do
26
26
  context 'with charset option' do
27
27
  let(:mime_type) { 'application/json; charset=utf8' }
28
28
 
29
- describe '#mime_type' do
29
+ describe '#content_type' do
30
30
  it 'should return Mime::JSON' do
31
- expect(response.mime_type).to be == Mime::JSON
31
+ expect(response.content_type).to be == Mime::JSON
32
32
  end
33
33
  end
34
34
 
data/spec/acfs_spec.rb CHANGED
@@ -3,11 +3,15 @@ require 'spec_helper'
3
3
  describe "Acfs" do
4
4
 
5
5
  before do
6
+ Acfs.clear
6
7
  Acfs.use Acfs::Middleware::JsonDecoder
8
+ Acfs.use Acfs::Middleware::MessagePackDecoder
7
9
 
8
10
  headers = { 'Content-Type' => 'application/json' }
9
11
  stub_request(:get, "http://users.example.org/users").to_return(:body => '[{"id":1,"name":"Anon","age":12},{"id":2,"name":"John","age":26}]', headers: headers)
10
12
  stub_request(:get, "http://users.example.org/users/2").to_return(:body => '{"id":2,"name":"John","age":26}', headers: headers)
13
+ stub_request(:get, "http://users.example.org/users/3").to_return(:body => '{"id":2,"name":"Miraculix","age":122}', headers: headers)
14
+ stub_request(:get, "http://users.example.org/users/100").to_return(:body => '{"id":2,"name":"Jimmy","age":45}', headers: headers)
11
15
  stub_request(:get, "http://users.example.org/users/2/friends").to_return(:body => '[{"id":1,"name":"Anon","age":12}]', headers: headers)
12
16
  stub_request(:get, "http://comments.example.org/comments?user=2").to_return(:body => '[{"id":1,"text":"Comment #1"},{"id":2,"text":"Comment #2"}]', headers: headers)
13
17
  end
@@ -24,6 +28,34 @@ describe "Acfs" do
24
28
  expect(@user.age).to be == 26
25
29
  end
26
30
 
31
+ it 'should load multiple single resources' do
32
+ @users = MyUser.find(2, 3, 100) do |users|
33
+ # This block should be called only after *all* resources are loaded.
34
+ @john = users[0]
35
+ @mirx = users[1]
36
+ @jimy = users[2]
37
+ end
38
+
39
+ expect(@users).to_not be_loaded
40
+
41
+ Acfs.run
42
+
43
+ expect(@users).to be_loaded
44
+ expect(@users).to have(3).items
45
+
46
+ expect(@users[0].name).to be == 'John'
47
+ expect(@users[0].age).to be == 26
48
+ expect(@users[0]).to be == @john
49
+
50
+ expect(@users[1].name).to be == 'Miraculix'
51
+ expect(@users[1].age).to be == 122
52
+ expect(@users[1]).to be == @mirx
53
+
54
+ expect(@users[2].name).to be == 'Jimmy'
55
+ expect(@users[2].age).to be == 45
56
+ expect(@users[2]).to be == @jimy
57
+ end
58
+
27
59
  it 'should load multiple resources' do
28
60
  @users = MyUser.all
29
61
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: acfs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jan Graichen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-04-10 00:00:00.000000000 Z
11
+ date: 2013-04-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -164,6 +164,20 @@ dependencies:
164
164
  - - '>='
165
165
  - !ruby/object:Gem::Version
166
166
  version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: msgpack
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - '>='
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - '>='
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
167
181
  description: API Client For Services
168
182
  email:
169
183
  - jg@altimos.de
@@ -188,6 +202,7 @@ files:
188
202
  - lib/acfs/collection.rb
189
203
  - lib/acfs/middleware/base.rb
190
204
  - lib/acfs/middleware/json_decoder.rb
205
+ - lib/acfs/middleware/msgpack_decoder.rb
191
206
  - lib/acfs/middleware/print.rb
192
207
  - lib/acfs/model.rb
193
208
  - lib/acfs/model/attributes.rb
@@ -207,6 +222,7 @@ files:
207
222
  - lib/acfs/service.rb
208
223
  - lib/acfs/version.rb
209
224
  - spec/acfs/middleware/json_decoder_spec.rb
225
+ - spec/acfs/middleware/msgpack_decoder_spec.rb
210
226
  - spec/acfs/request/callbacks_spec.rb
211
227
  - spec/acfs/request_spec.rb
212
228
  - spec/acfs/response/formats_spec.rb
@@ -243,6 +259,7 @@ specification_version: 4
243
259
  summary: An abstract API base client for service oriented application.
244
260
  test_files:
245
261
  - spec/acfs/middleware/json_decoder_spec.rb
262
+ - spec/acfs/middleware/msgpack_decoder_spec.rb
246
263
  - spec/acfs/request/callbacks_spec.rb
247
264
  - spec/acfs/request_spec.rb
248
265
  - spec/acfs/response/formats_spec.rb