acfs 0.5.1 → 0.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: 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