acfs 0.14.0 → 0.15.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/README.md +57 -14
- data/acfs.gemspec +1 -0
- data/lib/acfs.rb +10 -13
- data/lib/acfs/adapter/base.rb +28 -0
- data/lib/acfs/adapter/typhoeus.rb +10 -14
- data/lib/acfs/errors.rb +2 -0
- data/lib/acfs/global.rb +17 -0
- data/lib/acfs/model.rb +2 -0
- data/lib/acfs/model/operational.rb +19 -0
- data/lib/acfs/model/persistence.rb +2 -17
- data/lib/acfs/model/query_methods.rb +9 -23
- data/lib/acfs/operation.rb +60 -0
- data/lib/acfs/runner.rb +76 -0
- data/lib/acfs/service.rb +0 -2
- data/lib/acfs/service/middleware.rb +3 -3
- data/lib/acfs/stub.rb +85 -0
- data/lib/acfs/version.rb +1 -1
- data/spec/acfs/service/middleware_spec.rb +49 -0
- data/spec/acfs/stub_spec.rb +96 -0
- data/spec/acfs_spec.rb +11 -12
- data/spec/spec_helper.rb +2 -1
- data/spec/support/response.rb +1 -0
- metadata +26 -3
- data/lib/acfs/service/request_handler.rb +0 -27
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f5bd82876ac05bd67b6a8d95a8d9e0779eb7d1c1
|
4
|
+
data.tar.gz: 5234ed3bfb459596c3cc0533bec51a67013f0b69
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e10f336df5c116c5155ab4c51dbbe8a746ce3235b87180c69a5a1651ed285c75abeccd219b95fc0dda1a5e65bd9ccd3bfa74a42f672ae9de60d8ec1f9f99f14a
|
7
|
+
data.tar.gz: 8631fdb5b179084f4f54ca953aa750f6b70242c5ebcd6aa6c52f97836663dd88e3d98c6dc30239c9144a63718913d3bac4041530a3aaccebedf8567f4404ef4e
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -2,12 +2,9 @@
|
|
2
2
|
|
3
3
|
[![Gem Version](https://badge.fury.io/rb/acfs.png)](http://badge.fury.io/rb/acfs) [![Build Status](https://travis-ci.org/jgraichen/acfs.png?branch=master)](https://travis-ci.org/jgraichen/acfs) [![Coverage Status](https://coveralls.io/repos/jgraichen/acfs/badge.png?branch=master)](https://coveralls.io/r/jgraichen/acfs) [![Code Climate](https://codeclimate.com/github/jgraichen/acfs.png)](https://codeclimate.com/github/jgraichen/acfs) [![Dependency Status](https://gemnasium.com/jgraichen/acfs.png)](https://gemnasium.com/jgraichen/acfs)
|
4
4
|
|
5
|
-
Acfs is a library to develop API client libraries for single services within a
|
6
|
-
larger service oriented application.
|
5
|
+
Acfs is a library to develop API client libraries for single services within a larger service oriented application.
|
7
6
|
|
8
|
-
Acfs covers model and service abstraction, convenient query and filter methods,
|
9
|
-
full middleware stack for pre-processing requests and responses on a per service
|
10
|
-
level and automatic request queuing and parallel processing. See Usage for more.
|
7
|
+
Acfs covers model and service abstraction, convenient query and filter methods, full middleware stack for pre-processing requests and responses on a per service level and automatic request queuing and parallel processing. See Usage for more.
|
11
8
|
|
12
9
|
## Installation
|
13
10
|
|
@@ -15,8 +12,7 @@ Add this line to your application's Gemfile:
|
|
15
12
|
|
16
13
|
gem 'acfs', '~> 0.14.0'
|
17
14
|
|
18
|
-
**Note:** Acfs is under development. I'll try to avoid changes to the public
|
19
|
-
API but internal APIs may change quite often.
|
15
|
+
**Note:** Acfs is under development. I'll try to avoid changes to the public API but internal APIs may change quite often.
|
20
16
|
|
21
17
|
And then execute:
|
22
18
|
|
@@ -42,8 +38,7 @@ class UserService < Acfs::Service
|
|
42
38
|
end
|
43
39
|
```
|
44
40
|
|
45
|
-
This specifies where the `UserService` is located. You can now create some
|
46
|
-
models representing resources served by the `UserService`.
|
41
|
+
This specifies where the `UserService` is located. You can now create some models representing resources served by the `UserService`.
|
47
42
|
|
48
43
|
```ruby
|
49
44
|
class User
|
@@ -63,8 +58,7 @@ class User
|
|
63
58
|
end
|
64
59
|
```
|
65
60
|
|
66
|
-
The service and model classes can be shipped as a gem or git submodule to be
|
67
|
-
included by the frontend application(s).
|
61
|
+
The service and model classes can be shipped as a gem or git submodule to be included by the frontend application(s).
|
68
62
|
|
69
63
|
You can use the model there:
|
70
64
|
|
@@ -87,8 +81,7 @@ Acfs.run # Will request `http://users.myapp.org/users`
|
|
87
81
|
@users #=> [<User>, ...]
|
88
82
|
```
|
89
83
|
|
90
|
-
If you need multiple resources or dependent resources first define a "plan"
|
91
|
-
how they can be loaded:
|
84
|
+
If you need multiple resources or dependent resources first define a "plan" how they can be loaded:
|
92
85
|
|
93
86
|
```ruby
|
94
87
|
@user = User.find(5) do |user|
|
@@ -141,6 +134,57 @@ Acfs has basic update support using `PUT` requests:
|
|
141
134
|
@user.persisted? # => true
|
142
135
|
```
|
143
136
|
|
137
|
+
## Stubbing
|
138
|
+
|
139
|
+
You can stub resources in applications using an Acfs service client:
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
# Enable stubs in spec helper
|
143
|
+
Acfs::Stub.enable
|
144
|
+
|
145
|
+
before do
|
146
|
+
Acfs::Stub.resource MyUser, :read, with: { id: 1 }, return: { id: 1, name: 'John Smith', age: 32 }
|
147
|
+
Acfs::Stub.resource MyUser, :read, with: { id: 2 }, raise: :not_found
|
148
|
+
Acfs::Stub.resource Session, :create, with: { ident: 'john@exmaple.org', password: 's3cr3t' }, return: { id: 'longhash', user: 1 }
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'should find user number one' do
|
152
|
+
user = MyUser.find 1
|
153
|
+
Acfs.run
|
154
|
+
|
155
|
+
expect(user.id).to be == 1
|
156
|
+
expect(user.name).to be == 'John Smith'
|
157
|
+
expect(user.age).to be == 32
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'should not find user number two' do
|
161
|
+
MyUser.find 3
|
162
|
+
|
163
|
+
expect { Acfs.run }.to raise_error(Acfs::ResourceNotFound)
|
164
|
+
end
|
165
|
+
|
166
|
+
it 'should allow stub resource creation' do
|
167
|
+
session = Session.create! ident: 'john@exmaple.org', password: 's3cr3t'
|
168
|
+
|
169
|
+
expect(session.id).to be == 'longhash'
|
170
|
+
expect(session.user).to be == 1
|
171
|
+
end
|
172
|
+
```
|
173
|
+
|
174
|
+
By default Acfs raises an error when a non stubbed resource should be requested. You can switch of the behavior:
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
before do
|
178
|
+
Acfs::Stub.allow_requests = true
|
179
|
+
end
|
180
|
+
|
181
|
+
it 'should find user number one' do
|
182
|
+
user = MyUser.find 1
|
183
|
+
Acfs.run # Would have raised Acfs::RealRequestNotAllowedError
|
184
|
+
# Will run real request to user service instead.
|
185
|
+
end
|
186
|
+
```
|
187
|
+
|
144
188
|
## Roadmap
|
145
189
|
|
146
190
|
* Update
|
@@ -157,7 +201,6 @@ Acfs has basic update support using `PUT` requests:
|
|
157
201
|
Modified Headers, conflict detection, ...
|
158
202
|
* Pagination? Filtering? (If service API provides such features.)
|
159
203
|
* Messaging Queue support for services and models
|
160
|
-
* Allow stubbing of resources as objects for testing services.
|
161
204
|
* Documentation
|
162
205
|
|
163
206
|
## Contributing
|
data/acfs.gemspec
CHANGED
@@ -23,6 +23,7 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.add_runtime_dependency 'actionpack'
|
24
24
|
spec.add_runtime_dependency 'multi_json'
|
25
25
|
spec.add_runtime_dependency 'typhoeus'
|
26
|
+
spec.add_runtime_dependency 'rack'
|
26
27
|
|
27
28
|
spec.add_development_dependency 'bundler', '~> 1.3'
|
28
29
|
end
|
data/lib/acfs.rb
CHANGED
@@ -1,16 +1,24 @@
|
|
1
1
|
require 'active_support'
|
2
|
-
require 'active_support/core_ext'
|
2
|
+
require 'active_support/core_ext/class'
|
3
|
+
require 'active_support/core_ext/hash'
|
4
|
+
require 'active_support/core_ext/module'
|
5
|
+
|
3
6
|
require 'acfs/version'
|
4
7
|
require 'acfs/errors'
|
8
|
+
require 'acfs/global'
|
5
9
|
|
6
10
|
module Acfs
|
7
11
|
extend ActiveSupport::Autoload
|
12
|
+
extend Global
|
8
13
|
|
9
14
|
autoload :Collection
|
10
15
|
autoload :Model
|
11
16
|
autoload :Request
|
12
17
|
autoload :Response
|
13
18
|
autoload :Service
|
19
|
+
autoload :Stub
|
20
|
+
autoload :Operation
|
21
|
+
autoload :Runner
|
14
22
|
|
15
23
|
module Middleware
|
16
24
|
extend ActiveSupport::Autoload
|
@@ -27,19 +35,8 @@ module Acfs
|
|
27
35
|
module Adapter
|
28
36
|
extend ActiveSupport::Autoload
|
29
37
|
|
38
|
+
autoload :Base
|
30
39
|
autoload :Typhoeus
|
31
40
|
end
|
32
|
-
|
33
|
-
class << self
|
34
|
-
|
35
|
-
# Run all queued
|
36
|
-
def run
|
37
|
-
adapter.run
|
38
|
-
end
|
39
|
-
|
40
|
-
def adapter
|
41
|
-
@adapter ||= Adapter::Typhoeus.new
|
42
|
-
end
|
43
|
-
end
|
44
41
|
end
|
45
42
|
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Acfs::Adapter
|
2
|
+
|
3
|
+
# Base adapter handling operation queuing
|
4
|
+
# and processing.
|
5
|
+
#
|
6
|
+
class Base
|
7
|
+
|
8
|
+
# Start processing queued requests.
|
9
|
+
#
|
10
|
+
def start
|
11
|
+
end
|
12
|
+
|
13
|
+
# Abort running and queued requests.
|
14
|
+
#
|
15
|
+
def abort
|
16
|
+
end
|
17
|
+
|
18
|
+
# Run request right now skipping queue.
|
19
|
+
#
|
20
|
+
def run(_)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Enqueue request to be run later.
|
24
|
+
#
|
25
|
+
def queue(_)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -5,26 +5,22 @@ module Acfs
|
|
5
5
|
|
6
6
|
# Adapter for Typhoeus.
|
7
7
|
#
|
8
|
-
class Typhoeus
|
8
|
+
class Typhoeus < Base
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
return hydra.run unless request
|
10
|
+
def start
|
11
|
+
hydra.run
|
12
|
+
end
|
14
13
|
|
15
|
-
|
14
|
+
def abort
|
15
|
+
hydra.abort
|
16
16
|
end
|
17
17
|
|
18
|
-
|
19
|
-
|
20
|
-
def queue(req)
|
21
|
-
hydra.queue convert_request(req)
|
18
|
+
def run(request)
|
19
|
+
convert_request(request).run
|
22
20
|
end
|
23
21
|
|
24
|
-
|
25
|
-
|
26
|
-
def clear
|
27
|
-
hydra.abort
|
22
|
+
def queue(request)
|
23
|
+
hydra.queue convert_request request
|
28
24
|
end
|
29
25
|
|
30
26
|
protected
|
data/lib/acfs/errors.rb
CHANGED
data/lib/acfs/global.rb
ADDED
data/lib/acfs/model.rb
CHANGED
@@ -5,6 +5,7 @@ require 'acfs/model/dirty'
|
|
5
5
|
require 'acfs/model/loadable'
|
6
6
|
require 'acfs/model/locatable'
|
7
7
|
require 'acfs/model/persistence'
|
8
|
+
require 'acfs/model/operational'
|
8
9
|
require 'acfs/model/query_methods'
|
9
10
|
require 'acfs/model/relations'
|
10
11
|
require 'acfs/model/service'
|
@@ -30,6 +31,7 @@ module Acfs
|
|
30
31
|
include Model::Loadable
|
31
32
|
include Model::Persistence
|
32
33
|
include Model::Locatable
|
34
|
+
include Model::Operational
|
33
35
|
include Model::QueryMethods
|
34
36
|
include Model::Relations
|
35
37
|
include Model::Service
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Acfs::Model
|
2
|
+
|
3
|
+
# Provide methods for creating and processing CRUD operations and
|
4
|
+
# handling responses. That includes error handling as well as
|
5
|
+
# handling stubbed resources.
|
6
|
+
#
|
7
|
+
# Should only be used internal.
|
8
|
+
#
|
9
|
+
module Operational
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
delegate :operation, to: :'self.class'
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
def operation(action, opts = {}, &block)
|
15
|
+
Acfs.runner.process ::Acfs::Operation.new self, action, opts, &block
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -52,16 +52,9 @@ module Acfs
|
|
52
52
|
|
53
53
|
opts[:data] = attributes unless opts[:data]
|
54
54
|
|
55
|
-
|
56
|
-
|
57
|
-
if response.success?
|
58
|
-
update_with response.data
|
59
|
-
else
|
60
|
-
self.class.raise! response
|
61
|
-
end
|
55
|
+
operation (new? ? :create : :update), opts do |data|
|
56
|
+
update_with data
|
62
57
|
end
|
63
|
-
|
64
|
-
self.class.service.run request
|
65
58
|
end
|
66
59
|
|
67
60
|
module ClassMethods
|
@@ -96,14 +89,6 @@ module Acfs
|
|
96
89
|
self.attributes = data
|
97
90
|
loaded!
|
98
91
|
end
|
99
|
-
|
100
|
-
def create_request(opts = {})
|
101
|
-
Acfs::Request.new self.class.url, method: :post, data: opts[:data]
|
102
|
-
end
|
103
|
-
|
104
|
-
def put_request(opts = {})
|
105
|
-
Acfs::Request.new url, method: :put, data: opts[:data]
|
106
|
-
end
|
107
92
|
end
|
108
93
|
end
|
109
94
|
end
|
@@ -37,8 +37,8 @@ module Acfs::Model
|
|
37
37
|
def all(params = {}, &block)
|
38
38
|
collection = ::Acfs::Collection.new
|
39
39
|
|
40
|
-
|
41
|
-
|
40
|
+
operation :list, params: params do |data|
|
41
|
+
data.each do |obj|
|
42
42
|
collection << self.new.tap do |m|
|
43
43
|
m.attributes = obj
|
44
44
|
m.loaded!
|
@@ -47,37 +47,23 @@ module Acfs::Model
|
|
47
47
|
collection.loaded!
|
48
48
|
block.call collection unless block.nil?
|
49
49
|
end
|
50
|
-
service.queue request
|
51
50
|
|
52
51
|
collection
|
53
52
|
end
|
54
53
|
alias :where :all
|
55
54
|
|
56
|
-
def raise!(response)
|
57
|
-
case response.code
|
58
|
-
when 404
|
59
|
-
raise ::Acfs::ResourceNotFound.new response: response
|
60
|
-
when 422
|
61
|
-
raise ::Acfs::InvalidResource.new response: response, errors: response.data['errors']
|
62
|
-
else
|
63
|
-
raise ::Acfs::ErroneousResponse.new response: response
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
55
|
private
|
68
56
|
def find_single(id, opts, &block)
|
69
57
|
model = self.new
|
70
58
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
end
|
59
|
+
opts[:params] ||= {}
|
60
|
+
opts[:params].merge!({ id: id })
|
61
|
+
|
62
|
+
operation :read, opts do |data|
|
63
|
+
model.attributes = data
|
64
|
+
model.loaded!
|
65
|
+
block.call model unless block.nil?
|
79
66
|
end
|
80
|
-
service.queue request
|
81
67
|
|
82
68
|
model
|
83
69
|
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Acfs
|
2
|
+
|
3
|
+
# Describes a CRUD operation. Handle request creation and response
|
4
|
+
# processing as well as error handling and stubbing.
|
5
|
+
#
|
6
|
+
class Operation
|
7
|
+
attr_reader :action, :params, :resource, :data, :callback
|
8
|
+
delegate :service, to: :resource
|
9
|
+
|
10
|
+
def initialize(resource, action, opts = {}, &block)
|
11
|
+
@resource = resource
|
12
|
+
@action = action.to_sym
|
13
|
+
@params = opts[:params] || {}
|
14
|
+
@data = opts[:data] || {}
|
15
|
+
@callback = block
|
16
|
+
|
17
|
+
raise ArgumentError, 'ID parameter required for READ, UPDATE and DELETE operation.' if single? and id.nil?
|
18
|
+
end
|
19
|
+
|
20
|
+
def single?
|
21
|
+
[:read, :update, :delete].include? action
|
22
|
+
end
|
23
|
+
|
24
|
+
def synchronous?
|
25
|
+
[:update, :delete, :create].include? action
|
26
|
+
end
|
27
|
+
|
28
|
+
def id
|
29
|
+
@id ||= params.delete(:id) || data[:id]
|
30
|
+
end
|
31
|
+
|
32
|
+
def url
|
33
|
+
single? ? resource.url(id) : resource.url
|
34
|
+
end
|
35
|
+
|
36
|
+
def method
|
37
|
+
{ read: :get, list: :get, update: :put, create: :post, delete: :delete }[action]
|
38
|
+
end
|
39
|
+
|
40
|
+
def request
|
41
|
+
request = ::Acfs::Request.new url, method: method, params: params, data: data
|
42
|
+
request.on_complete do |response|
|
43
|
+
handle_failure response unless response.success?
|
44
|
+
callback.call response.data
|
45
|
+
end
|
46
|
+
request
|
47
|
+
end
|
48
|
+
|
49
|
+
def handle_failure(response)
|
50
|
+
case response.code
|
51
|
+
when 404
|
52
|
+
raise ::Acfs::ResourceNotFound.new response: response
|
53
|
+
when 422
|
54
|
+
raise ::Acfs::InvalidResource.new response: response, errors: response.data.try(:[], 'errors')
|
55
|
+
else
|
56
|
+
raise ::Acfs::ErroneousResponse.new response: response
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/lib/acfs/runner.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'acfs/service/middleware'
|
2
|
+
|
3
|
+
module Acfs
|
4
|
+
|
5
|
+
class Runner
|
6
|
+
include Service::Middleware
|
7
|
+
attr_reader :adapter
|
8
|
+
|
9
|
+
def initialize(adapter)
|
10
|
+
@adapter = adapter
|
11
|
+
@running = false
|
12
|
+
end
|
13
|
+
|
14
|
+
# Process an operation. Synchronous operations will be run
|
15
|
+
# and parallel operations will be queued.
|
16
|
+
#
|
17
|
+
def process(op)
|
18
|
+
op.synchronous? ? run(op) : enqueue(op)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Run operation right now skipping queue.
|
22
|
+
#
|
23
|
+
def run(op)
|
24
|
+
op_request(op) { |req| adapter.run req }
|
25
|
+
end
|
26
|
+
|
27
|
+
# List of current queued operations.
|
28
|
+
#
|
29
|
+
def queue
|
30
|
+
@queue ||= []
|
31
|
+
end
|
32
|
+
|
33
|
+
# Enqueue operation to be run later.
|
34
|
+
#
|
35
|
+
def enqueue(op)
|
36
|
+
if running?
|
37
|
+
op_request(op) { |req| adapter.queue req }
|
38
|
+
else
|
39
|
+
queue << op
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Return true if queued operations are currently processed.
|
44
|
+
#
|
45
|
+
def running?
|
46
|
+
@running
|
47
|
+
end
|
48
|
+
|
49
|
+
# Start processing queued operations.
|
50
|
+
#
|
51
|
+
def start
|
52
|
+
enqueue_operations
|
53
|
+
|
54
|
+
@running = true
|
55
|
+
adapter.start
|
56
|
+
@running = false
|
57
|
+
end
|
58
|
+
|
59
|
+
def clear
|
60
|
+
queue.clear
|
61
|
+
adapter.abort
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
def enqueue_operations
|
66
|
+
while (op = queue.shift)
|
67
|
+
op_request(op) { |req| adapter.queue req }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def op_request(op)
|
72
|
+
return if Acfs::Stub.enabled? and Acfs::Stub.stubbed(op)
|
73
|
+
yield prepare op.service.prepare op.request
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
data/lib/acfs/service.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'acfs/service/request_handler'
|
2
1
|
require 'acfs/service/middleware'
|
3
2
|
|
4
3
|
module Acfs
|
@@ -9,7 +8,6 @@ module Acfs
|
|
9
8
|
attr_accessor :options
|
10
9
|
class_attribute :base_url
|
11
10
|
|
12
|
-
include Service::RequestHandler
|
13
11
|
include Service::Middleware
|
14
12
|
|
15
13
|
def initialize(options = {})
|
@@ -8,8 +8,8 @@ module Acfs
|
|
8
8
|
module Middleware
|
9
9
|
extend ActiveSupport::Concern
|
10
10
|
|
11
|
-
def prepare(
|
12
|
-
self.class.middleware.call
|
11
|
+
def prepare(request) # :nodoc:
|
12
|
+
self.class.middleware.call request
|
13
13
|
end
|
14
14
|
|
15
15
|
module ClassMethods
|
@@ -40,7 +40,7 @@ module Acfs
|
|
40
40
|
#
|
41
41
|
def clear
|
42
42
|
@middleware = nil
|
43
|
-
@middlewares =
|
43
|
+
@middlewares = []
|
44
44
|
end
|
45
45
|
end
|
46
46
|
end
|
data/lib/acfs/stub.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'rack/utils'
|
2
|
+
|
3
|
+
module Acfs
|
4
|
+
|
5
|
+
# Global handler for stubbing resources.
|
6
|
+
#
|
7
|
+
class Stub
|
8
|
+
ACTIONS = [ :read, :create, :update, :delete, :list ]
|
9
|
+
|
10
|
+
class << self
|
11
|
+
|
12
|
+
# Stub a resource with given handler block. An already created handler
|
13
|
+
# for same resource class will be overridden.
|
14
|
+
#
|
15
|
+
def resource(klass, action, opts = {}, &block)
|
16
|
+
action = action.to_sym
|
17
|
+
raise ArgumentError, "Unknown action `#{action}`." unless ACTIONS.include? action
|
18
|
+
|
19
|
+
stubs[klass] ||= {}
|
20
|
+
stubs[klass][action] ||= []
|
21
|
+
stubs[klass][action] << opts
|
22
|
+
end
|
23
|
+
|
24
|
+
def allow_requests=(allow)
|
25
|
+
@allow_requests = allow ? true : false
|
26
|
+
end
|
27
|
+
|
28
|
+
def allow_requests?
|
29
|
+
@allow_requests ||= false
|
30
|
+
end
|
31
|
+
|
32
|
+
def enabled?
|
33
|
+
@enabled ||= false
|
34
|
+
end
|
35
|
+
|
36
|
+
def enable; @enabled = true end
|
37
|
+
def disable; @enabled = false end
|
38
|
+
|
39
|
+
# Clear all stubs.
|
40
|
+
#
|
41
|
+
def clear(klass = nil)
|
42
|
+
klass.nil? ? stubs.clear : stubs[klass].try(:clear)
|
43
|
+
end
|
44
|
+
|
45
|
+
def stubs
|
46
|
+
@stubs ||= {}
|
47
|
+
end
|
48
|
+
|
49
|
+
def stub_for(op)
|
50
|
+
return false unless (classes = stubs[op.resource])
|
51
|
+
return false unless (actions = classes[op.action])
|
52
|
+
|
53
|
+
params = op.params
|
54
|
+
params.merge! id: op.id unless op.id.nil?
|
55
|
+
actions.select! { |stub| stub[:with] == params || stub[:with] == op.data }
|
56
|
+
actions.first
|
57
|
+
end
|
58
|
+
|
59
|
+
def stubbed(op)
|
60
|
+
stub = stub_for op
|
61
|
+
unless stub
|
62
|
+
return false if allow_requests?
|
63
|
+
raise RealRequestNotAllowedError, "No stub found for `#{op.resource.name}` with params `#{op.params}`"
|
64
|
+
end
|
65
|
+
|
66
|
+
if (data = stub[:return])
|
67
|
+
op.callback.call data
|
68
|
+
elsif (err = stub[:raise])
|
69
|
+
raise_error op, err, stub[:return]
|
70
|
+
else
|
71
|
+
raise ArgumentError, 'Unsupported stub.'
|
72
|
+
end
|
73
|
+
|
74
|
+
true
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
def raise_error(op, name, data)
|
79
|
+
raise name if name.is_a? Class
|
80
|
+
|
81
|
+
op.handle_failure ::Acfs::Response.new nil, status: Rack::Utils.status_code(name), data: data
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/lib/acfs/version.rb
CHANGED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class TestMiddleware < Acfs::Middleware::Base
|
4
|
+
end
|
5
|
+
|
6
|
+
describe Acfs::Service::Middleware do
|
7
|
+
let(:srv_class) { Class.new(Acfs::Service) }
|
8
|
+
let(:options) { {} }
|
9
|
+
let(:service) { srv_class.new options }
|
10
|
+
let(:middleware) { TestMiddleware }
|
11
|
+
|
12
|
+
describe '.use' do
|
13
|
+
let(:options) { { abc: 'cde' } }
|
14
|
+
|
15
|
+
it 'should add middleware to list' do
|
16
|
+
srv_class.use middleware
|
17
|
+
|
18
|
+
expect(srv_class.instance_variable_get(:@middlewares)).to include(middleware)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should add middleware to stack' do
|
22
|
+
srv_class.use middleware
|
23
|
+
|
24
|
+
expect(srv_class.middleware).to be_a(middleware)
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'should instantiate middleware object' do
|
28
|
+
middleware.should_receive(:new).with(anything, options)
|
29
|
+
|
30
|
+
srv_class.use middleware, options
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe '.clear' do
|
35
|
+
before { srv_class.use middleware }
|
36
|
+
|
37
|
+
it 'should clear middleware list' do
|
38
|
+
srv_class.clear
|
39
|
+
|
40
|
+
expect(srv_class.instance_variable_get(:@middlewares)).to be_empty
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'should reset middleware stack' do
|
44
|
+
srv_class.clear
|
45
|
+
|
46
|
+
expect(srv_class.instance_variable_get(:@middleware)).to be_nil
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class SpecialCustomError < StandardError; end
|
4
|
+
|
5
|
+
describe Acfs::Stub do
|
6
|
+
let(:stub) { Class.new(Acfs::Stub) }
|
7
|
+
|
8
|
+
before(:all) { Acfs::Stub.enable }
|
9
|
+
after(:all) { Acfs::Stub.disable }
|
10
|
+
|
11
|
+
before do
|
12
|
+
Acfs::Stub.allow_requests = false
|
13
|
+
|
14
|
+
#Acfs::Stub.read(MyUser).with(id: 5).and_return({ id: 5, name: 'John', age: 32 })
|
15
|
+
#Acfs::Stub.read(MyUser).with(id: 6).and_raise(:not_found)
|
16
|
+
#Acfs::Stub.create(MyUser).with(name: '', age: 12).and_return(:invalid, errors: { name: [ 'must be present ']})
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '.resource' do
|
20
|
+
context 'with read action' do
|
21
|
+
before do
|
22
|
+
Acfs::Stub.resource MyUser, :read, with: { id: 1 }, return: { id: 1, name: 'John Smith', age: 32 }
|
23
|
+
Acfs::Stub.resource MyUser, :read, with: { id: 2 }, raise: SpecialCustomError
|
24
|
+
Acfs::Stub.resource MyUser, :read, with: { id: 3 }, raise: :not_found
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'should allow to stub resource reads' do
|
28
|
+
user = MyUser.find 1
|
29
|
+
Acfs.run
|
30
|
+
|
31
|
+
expect(user.id).to be == 1
|
32
|
+
expect(user.name).to be == 'John Smith'
|
33
|
+
expect(user.age).to be == 32
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'with error' do
|
37
|
+
it 'should allow to raise errors' do
|
38
|
+
MyUser.find 2
|
39
|
+
|
40
|
+
expect { Acfs.run }.to raise_error(SpecialCustomError)
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'should allow to raise symbolic errors' do
|
44
|
+
MyUser.find 3
|
45
|
+
|
46
|
+
expect { Acfs.run }.to raise_error(Acfs::ResourceNotFound)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context 'with create action' do
|
52
|
+
before do
|
53
|
+
Acfs::Stub.resource Session, :create, with: { ident: 'john@exmaple.org', password: 's3cr3t' }, return: { id: 'longhash', user: 1 }
|
54
|
+
Acfs::Stub.resource Session, :create, with: { ident: 'john@exmaple.org', password: 'wrong' }, raise: 422
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'should allow stub resource creation' do
|
58
|
+
session = Session.create! ident: 'john@exmaple.org', password: 's3cr3t'
|
59
|
+
|
60
|
+
expect(session.id).to be == 'longhash'
|
61
|
+
expect(session.user).to be == 1
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should allow to raise error' do
|
65
|
+
expect {
|
66
|
+
Session.create! ident: 'john@exmaple.org', password: 'wrong'
|
67
|
+
}.to raise_error(::Acfs::InvalidResource)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe '.allow_requests=' do
|
73
|
+
context 'when enabled' do
|
74
|
+
before do
|
75
|
+
Acfs::Stub.allow_requests = true
|
76
|
+
stub_request(:get, 'http://users.example.org/users/2').to_return response({ id: 2, name: 'John', age: 26 })
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'should allow real requests' do
|
80
|
+
@user = MyUser.find 2
|
81
|
+
expect { Acfs.run }.to_not raise_error
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
context 'when disabled' do
|
86
|
+
before do
|
87
|
+
Acfs::Stub.allow_requests = false
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'should allow real requests' do
|
91
|
+
@user = MyUser.find 2
|
92
|
+
expect { Acfs.run }.to raise_error(Acfs::RealRequestNotAllowedError)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
data/spec/acfs_spec.rb
CHANGED
@@ -1,19 +1,18 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe
|
3
|
+
describe 'Acfs' do
|
4
4
|
|
5
5
|
before do
|
6
|
-
|
7
|
-
stub_request(:get,
|
8
|
-
stub_request(:get,
|
9
|
-
stub_request(:get,
|
10
|
-
stub_request(:get,
|
11
|
-
stub_request(:get,
|
12
|
-
stub_request(:get, "http://comments.example.org/comments?user=2").to_return response([{ id: 1, text: "Comment #1"}, { id: 2, text: "Comment #2" }])
|
6
|
+
stub_request(:get, 'http://users.example.org/users').to_return response([{ id: 1, name: 'Anon', age: 12 }, { id: 2, name: 'John', age: 26 }])
|
7
|
+
stub_request(:get, 'http://users.example.org/users/2').to_return response({ id: 2, name: 'John', age: 26 })
|
8
|
+
stub_request(:get, 'http://users.example.org/users/3').to_return response({ id: 3, name: 'Miraculix', age: 122 })
|
9
|
+
stub_request(:get, 'http://users.example.org/users/100').to_return response({ id:100, name: 'Jimmy', age: 45 })
|
10
|
+
stub_request(:get, 'http://users.example.org/users/2/friends').to_return response([{ id: 1, name: 'Anon', age: 12 }])
|
11
|
+
stub_request(:get, 'http://comments.example.org/comments?user=2').to_return response([{ id: 1, text: 'Comment #1' }, { id: 2, text: 'Comment #2' }])
|
13
12
|
end
|
14
13
|
|
15
14
|
it 'should update single resource synchronously' do
|
16
|
-
stub = stub_request(:put,
|
15
|
+
stub = stub_request(:put, 'http://users.example.org/users/2')
|
17
16
|
.to_return { |request| { body: request.body, headers: {'Content-Type' => request.headers['Content-Type']}} }
|
18
17
|
|
19
18
|
@user = MyUser.find 2
|
@@ -22,7 +21,7 @@ describe "Acfs" do
|
|
22
21
|
expect(@user).to_not be_changed
|
23
22
|
expect(@user).to be_persisted
|
24
23
|
|
25
|
-
@user.name =
|
24
|
+
@user.name = 'Johnny'
|
26
25
|
|
27
26
|
expect(@user).to be_changed
|
28
27
|
expect(@user).to_not be_persisted
|
@@ -35,7 +34,7 @@ describe "Acfs" do
|
|
35
34
|
end
|
36
35
|
|
37
36
|
it 'should create a single resource synchronously' do
|
38
|
-
stub = stub_request(:post,
|
37
|
+
stub = stub_request(:post, 'http://users.example.org/sessions').to_return response({id: 'sessionhash', user: 1})
|
39
38
|
|
40
39
|
session = Session.create ident: 'Anon'
|
41
40
|
|
@@ -104,7 +103,7 @@ describe "Acfs" do
|
|
104
103
|
end
|
105
104
|
|
106
105
|
it 'should load associated resources' do
|
107
|
-
pending
|
106
|
+
pending 'TODO: Implement high level feature'
|
108
107
|
|
109
108
|
@user = MyUser.find(2) do |user|
|
110
109
|
@friends = user.friends.all
|
data/spec/spec_helper.rb
CHANGED
data/spec/support/response.rb
CHANGED
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.
|
4
|
+
version: 0.15.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-05-
|
11
|
+
date: 2013-05-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -80,6 +80,20 @@ dependencies:
|
|
80
80
|
- - '>='
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rack
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
83
97
|
- !ruby/object:Gem::Dependency
|
84
98
|
name: bundler
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -113,9 +127,11 @@ files:
|
|
113
127
|
- gemfiles/Gemfile.rails-3-2
|
114
128
|
- gemfiles/Gemfile.rails-4-0
|
115
129
|
- lib/acfs.rb
|
130
|
+
- lib/acfs/adapter/base.rb
|
116
131
|
- lib/acfs/adapter/typhoeus.rb
|
117
132
|
- lib/acfs/collection.rb
|
118
133
|
- lib/acfs/errors.rb
|
134
|
+
- lib/acfs/global.rb
|
119
135
|
- lib/acfs/middleware/base.rb
|
120
136
|
- lib/acfs/middleware/json_decoder.rb
|
121
137
|
- lib/acfs/middleware/json_encoder.rb
|
@@ -132,18 +148,21 @@ files:
|
|
132
148
|
- lib/acfs/model/initialization.rb
|
133
149
|
- lib/acfs/model/loadable.rb
|
134
150
|
- lib/acfs/model/locatable.rb
|
151
|
+
- lib/acfs/model/operational.rb
|
135
152
|
- lib/acfs/model/persistence.rb
|
136
153
|
- lib/acfs/model/query_methods.rb
|
137
154
|
- lib/acfs/model/relations.rb
|
138
155
|
- lib/acfs/model/service.rb
|
156
|
+
- lib/acfs/operation.rb
|
139
157
|
- lib/acfs/request.rb
|
140
158
|
- lib/acfs/request/callbacks.rb
|
141
159
|
- lib/acfs/response.rb
|
142
160
|
- lib/acfs/response/formats.rb
|
143
161
|
- lib/acfs/response/status.rb
|
162
|
+
- lib/acfs/runner.rb
|
144
163
|
- lib/acfs/service.rb
|
145
164
|
- lib/acfs/service/middleware.rb
|
146
|
-
- lib/acfs/
|
165
|
+
- lib/acfs/stub.rb
|
147
166
|
- lib/acfs/version.rb
|
148
167
|
- spec/acfs/middleware/json_decoder_spec.rb
|
149
168
|
- spec/acfs/middleware/msgpack_decoder_spec.rb
|
@@ -158,7 +177,9 @@ files:
|
|
158
177
|
- spec/acfs/request_spec.rb
|
159
178
|
- spec/acfs/response/formats_spec.rb
|
160
179
|
- spec/acfs/response/status_spec.rb
|
180
|
+
- spec/acfs/service/middleware_spec.rb
|
161
181
|
- spec/acfs/service_spec.rb
|
182
|
+
- spec/acfs/stub_spec.rb
|
162
183
|
- spec/acfs_spec.rb
|
163
184
|
- spec/spec_helper.rb
|
164
185
|
- spec/support/response.rb
|
@@ -201,7 +222,9 @@ test_files:
|
|
201
222
|
- spec/acfs/request_spec.rb
|
202
223
|
- spec/acfs/response/formats_spec.rb
|
203
224
|
- spec/acfs/response/status_spec.rb
|
225
|
+
- spec/acfs/service/middleware_spec.rb
|
204
226
|
- spec/acfs/service_spec.rb
|
227
|
+
- spec/acfs/stub_spec.rb
|
205
228
|
- spec/acfs_spec.rb
|
206
229
|
- spec/spec_helper.rb
|
207
230
|
- spec/support/response.rb
|
@@ -1,27 +0,0 @@
|
|
1
|
-
module Acfs
|
2
|
-
class Service
|
3
|
-
|
4
|
-
# Methods to queue or executed request through this service.
|
5
|
-
#
|
6
|
-
module RequestHandler
|
7
|
-
|
8
|
-
# Queue a new request on global adapter queue.
|
9
|
-
#
|
10
|
-
def queue(request)
|
11
|
-
Acfs.adapter.queue prepare request
|
12
|
-
end
|
13
|
-
|
14
|
-
# Executes a request now.
|
15
|
-
#
|
16
|
-
def run(request)
|
17
|
-
Acfs.adapter.run prepare request
|
18
|
-
end
|
19
|
-
|
20
|
-
# Prepares a request to be processed through this service.
|
21
|
-
#
|
22
|
-
def prepare(request)
|
23
|
-
request
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|