acfs 0.30.0.1.b260 → 0.30.0.1.b261
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +8 -8
- data/lib/acfs.rb +1 -0
- data/lib/acfs/location.rb +75 -0
- data/lib/acfs/model/locatable.rb +89 -11
- data/lib/acfs/operation.rb +5 -7
- data/lib/acfs/request.rb +1 -1
- data/lib/acfs/service.rb +9 -6
- data/lib/acfs/singleton_resource.rb +16 -0
- data/lib/acfs/stub.rb +24 -1
- data/spec/acfs/model/locatable_spec.rb +33 -3
- data/spec/acfs/service_spec.rb +6 -3
- data/spec/support/service.rb +7 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
MDBhNGMzOTliN2YzYWNiZjZiMmM5ZjZhODMxYzk5NmU4MDU2ODA2Mg==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
NTAyZDVkZDEzOWFhY2I1OTM0OTQwMjk5MmE4YzIyZTQ4ZmJhNmY2MA==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
N2M4YTAwOTZmMWNkY2E1NWVjYzcxNjFlY2QwN2MwMGU5YmM5ZWEwNjA0MTEy
|
10
|
+
OTY4OWZhZjliZjI1ZGM3ZWY2OGE1ZjUwNjc5NjUyMTI1OWI2NDUyODgyNDg1
|
11
|
+
MmU3ODYxMmZhNTAyMWUxOGFiZjA2YzRjMWNkNjgxNmU2YWIxNjY=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
MmEyYWRlODQyODNlNWNkNTQ2NmFjZjhiNDliNTc1ZTA5M2QwMWRiNTljOTUz
|
14
|
+
YjBmYmQzMGQ1M2Q3ZjYyZWUxM2U5OTAwMDIwZDZhMWU3Y2QyMmRmYzMxMmJl
|
15
|
+
MDdmOTRjMmJmYjRiMjUxYmY4YzI1YmZmNDhhMjRhNmNkMTczN2M=
|
data/lib/acfs.rb
CHANGED
@@ -0,0 +1,75 @@
|
|
1
|
+
module Acfs
|
2
|
+
|
3
|
+
# @api private
|
4
|
+
#
|
5
|
+
# Describes a URL with placeholders.
|
6
|
+
#
|
7
|
+
class Location
|
8
|
+
attr_reader :arguments, :raw, :struct, :args
|
9
|
+
|
10
|
+
REGEXP= /^:([A-z][A-z0-9_]*)$/
|
11
|
+
|
12
|
+
def initialize(uri, args = {})
|
13
|
+
@raw = URI.parse uri
|
14
|
+
@args = args
|
15
|
+
@struct = raw.path.split('/').reject(&:empty?).map{|s| s =~ REGEXP ? $1.to_sym : s }
|
16
|
+
@arguments = struct.select{|s| Symbol === s }
|
17
|
+
end
|
18
|
+
|
19
|
+
def build(args = {})
|
20
|
+
unless Hash === args
|
21
|
+
raise ArgumentError.new "URI path arguments must be a hash, `#{args.inspect}' given."
|
22
|
+
end
|
23
|
+
|
24
|
+
self.class.new raw.to_s, args.merge(self.args)
|
25
|
+
end
|
26
|
+
|
27
|
+
def extract_from(*args)
|
28
|
+
args = Hash.new.tap do |collect|
|
29
|
+
arguments.each{|key| collect[key] = extract_arg key, args }
|
30
|
+
end
|
31
|
+
|
32
|
+
build args
|
33
|
+
end
|
34
|
+
|
35
|
+
def str
|
36
|
+
uri = raw.dup
|
37
|
+
uri.path = '/' + struct.map{|s| lookup_arg(s, args) }.join('/')
|
38
|
+
uri.to_s
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_s
|
42
|
+
raw.to_s
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def extract_arg(key, hashes)
|
47
|
+
hashes.each_with_index do |hash, index|
|
48
|
+
return (index == 0 ? hash.delete(key) : hash.fetch(key)) if hash.has_key?(key)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def lookup_arg(arg, args)
|
53
|
+
Symbol === arg ? lookup_replacement(arg, args) : arg
|
54
|
+
end
|
55
|
+
|
56
|
+
def lookup_replacement(sym, args)
|
57
|
+
value = get_replacement(sym, args).to_s
|
58
|
+
return value unless value.empty?
|
59
|
+
|
60
|
+
raise ArgumentError.new "Cannot replace path argument `#{sym}' with empty string."
|
61
|
+
end
|
62
|
+
|
63
|
+
def get_replacement(sym, args)
|
64
|
+
args.fetch(sym.to_s) do
|
65
|
+
args.fetch(sym) do
|
66
|
+
if args[:raise].nil? || args[:raise]
|
67
|
+
raise ArgumentError.new "URI path argument `#{sym}' missing on `#{to_s}'. Given: `#{args}.inspect'"
|
68
|
+
else
|
69
|
+
":#{sym}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/lib/acfs/model/locatable.rb
CHANGED
@@ -14,18 +14,83 @@ module Acfs::Model
|
|
14
14
|
|
15
15
|
module ClassMethods
|
16
16
|
|
17
|
-
#
|
17
|
+
# @overload url(suffix)
|
18
|
+
# @deprecated
|
19
|
+
# Return URL for this class of resource. Given suffix will be appended.
|
20
|
+
#
|
21
|
+
# @example
|
22
|
+
# User.url # => "http://users.srv.org/users"
|
23
|
+
# User.url(5) # => "http://users.srv.org/users/5"
|
24
|
+
#
|
25
|
+
# @param [ String ] suffix Suffix to append to URL.
|
26
|
+
# @return [ String ] Generated URL.
|
27
|
+
#
|
28
|
+
# @overload url(opts = {})
|
29
|
+
# Return URL for this class of resources. Given options will be used to replace
|
30
|
+
# URL path arguments and to determine the operation action.
|
31
|
+
#
|
32
|
+
# @example
|
33
|
+
# User.url(id: 5, action: :read) # => "http://users.srv.org/users/5"
|
34
|
+
# User.url(action: :list) # => "http://users.srv.org/users"
|
35
|
+
#
|
36
|
+
# @param opts [Hash] Options.
|
37
|
+
# @option opts [Symbol] :action Operation action, usually `:list`, `:create`, `:read`,
|
38
|
+
# `:update` or`:delete`.
|
39
|
+
# @return [ String ] Generated URL.
|
40
|
+
#
|
41
|
+
def url(suffix = nil, opts = {})
|
42
|
+
if Hash === suffix
|
43
|
+
opts, suffix = suffix, nil
|
44
|
+
end
|
45
|
+
|
46
|
+
opts[:action] = :list if suffix
|
47
|
+
|
48
|
+
url = location(opts).build(opts).str
|
49
|
+
url += "/#{suffix}" if suffix.to_s.present?
|
50
|
+
url
|
51
|
+
end
|
52
|
+
|
53
|
+
# Return a location object able to build the URL for this resource and
|
54
|
+
# given action.
|
18
55
|
#
|
19
56
|
# @example
|
20
|
-
#
|
21
|
-
#
|
57
|
+
# class Identity < ::Acfs::Resource
|
58
|
+
# service MyService, path: 'users/:user_id/identities'
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# location = Identity.location(action: :read)
|
62
|
+
# location.arguments
|
63
|
+
# => [:user_id, :id]
|
22
64
|
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
# @see Acfs::Service#url_for Delegates to Service#url_for with `suffix` option.
|
65
|
+
# location.raw_url
|
66
|
+
# => 'http://service/users/:user_id/identities/:id'
|
26
67
|
#
|
27
|
-
|
28
|
-
|
68
|
+
# location = Identity.location(action: :list)
|
69
|
+
# location.arguments
|
70
|
+
# => [:user_id]
|
71
|
+
#
|
72
|
+
# location.build(user_id: 42)
|
73
|
+
# => 'http://service/users/42/identities'
|
74
|
+
#
|
75
|
+
# @param opts [Hash] Options.
|
76
|
+
# @option opts [Symbol] :action Operation action, usually `:list`, `:create`, `:read`,
|
77
|
+
# `:update` or`:delete`.
|
78
|
+
#
|
79
|
+
# @return [Location] Location object.
|
80
|
+
#
|
81
|
+
def location(opts = {})
|
82
|
+
service.location(self, opts)
|
83
|
+
end
|
84
|
+
|
85
|
+
# @api private
|
86
|
+
def path_defaults
|
87
|
+
{
|
88
|
+
list: '/:path',
|
89
|
+
create: '/:path',
|
90
|
+
read: '/:path/:id',
|
91
|
+
update: '/:path/:id',
|
92
|
+
delete: '/:path/:id'
|
93
|
+
}
|
29
94
|
end
|
30
95
|
end
|
31
96
|
|
@@ -42,9 +107,22 @@ module Acfs::Model
|
|
42
107
|
# @return [ String ] Generated URL.
|
43
108
|
# @see ClassMethods#url
|
44
109
|
#
|
45
|
-
def url
|
46
|
-
return nil if
|
47
|
-
|
110
|
+
def url(opts = {})
|
111
|
+
return nil if need_primary_key? && !has_primary_key?
|
112
|
+
|
113
|
+
self.class.service.location(self.class, opts.reverse_merge(action: :read)).build(attributes).str
|
114
|
+
end
|
115
|
+
|
116
|
+
# @api private
|
117
|
+
# Return true if resource needs a primary key (id) for singular actions.
|
118
|
+
def need_primary_key?
|
119
|
+
true
|
120
|
+
end
|
121
|
+
|
122
|
+
# @api private
|
123
|
+
# Return true if resource has a primary key (id) set.
|
124
|
+
def has_primary_key?
|
125
|
+
respond_to?(:id) && !id.nil?
|
48
126
|
end
|
49
127
|
end
|
50
128
|
end
|
data/lib/acfs/operation.rb
CHANGED
@@ -6,7 +6,7 @@ module Acfs
|
|
6
6
|
# processing as well as error handling and stubbing.
|
7
7
|
#
|
8
8
|
class Operation
|
9
|
-
attr_reader :action, :params, :resource, :data, :callback
|
9
|
+
attr_reader :action, :params, :resource, :data, :callback, :location
|
10
10
|
delegate :service, to: :resource
|
11
11
|
delegate :call, to: :callback
|
12
12
|
|
@@ -19,6 +19,8 @@ module Acfs
|
|
19
19
|
@params = (opts[:params] || {}).dup
|
20
20
|
@data = (opts[:data] || {}).dup
|
21
21
|
|
22
|
+
@location = resource.location(action: @action).extract_from(@params, @data)
|
23
|
+
|
22
24
|
@callback = block
|
23
25
|
end
|
24
26
|
|
@@ -36,11 +38,7 @@ module Acfs
|
|
36
38
|
end
|
37
39
|
|
38
40
|
def full_params
|
39
|
-
id ? params.merge(id: id) : params
|
40
|
-
end
|
41
|
-
|
42
|
-
def url
|
43
|
-
single? ? resource.url(id) : resource.url
|
41
|
+
(id ? params.merge(id: id) : params).merge location.args
|
44
42
|
end
|
45
43
|
|
46
44
|
def method
|
@@ -48,7 +46,7 @@ module Acfs
|
|
48
46
|
end
|
49
47
|
|
50
48
|
def request
|
51
|
-
request = ::Acfs::Request.new
|
49
|
+
request = ::Acfs::Request.new location.str, method: method, params: params, data: data
|
52
50
|
request.on_complete do |response|
|
53
51
|
handle_failure response unless response.success?
|
54
52
|
callback.call response.data
|
data/lib/acfs/request.rb
CHANGED
@@ -12,7 +12,7 @@ module Acfs
|
|
12
12
|
include Request::Callbacks
|
13
13
|
|
14
14
|
def initialize(url, options = {}, &block)
|
15
|
-
@url = URI.parse(url).tap do |url|
|
15
|
+
@url = URI.parse(url.to_s).tap do |url|
|
16
16
|
@data = options.delete(:data) || nil
|
17
17
|
@format = options.delete(:format) || :json
|
18
18
|
@headers = options.delete(:headers) || {}
|
data/lib/acfs/service.rb
CHANGED
@@ -32,15 +32,18 @@ module Acfs
|
|
32
32
|
end
|
33
33
|
|
34
34
|
# @api private
|
35
|
-
# @return [
|
35
|
+
# @return [Location]
|
36
36
|
#
|
37
|
-
def
|
38
|
-
|
37
|
+
def location(resource_class, opts = {})
|
38
|
+
opts.reverse_merge! self.options
|
39
39
|
|
40
40
|
url = self.class.base_url.to_s
|
41
|
-
url +=
|
42
|
-
|
43
|
-
|
41
|
+
url += resource_class.path_defaults[opts[:action] || :list] || '/:path'
|
42
|
+
|
43
|
+
path = opts[:path]
|
44
|
+
path ||= (resource_class.name || 'class').pluralize.underscore
|
45
|
+
|
46
|
+
Location.new Location.new(url).build(raise: false, path: path).str
|
44
47
|
end
|
45
48
|
|
46
49
|
class << self
|
@@ -49,6 +49,11 @@ module Acfs
|
|
49
49
|
end
|
50
50
|
end
|
51
51
|
|
52
|
+
# @api private
|
53
|
+
def need_primary_key?
|
54
|
+
false
|
55
|
+
end
|
56
|
+
|
52
57
|
class << self
|
53
58
|
# @api public
|
54
59
|
#
|
@@ -90,6 +95,17 @@ module Acfs
|
|
90
95
|
undef_method :where
|
91
96
|
undef_method :find_by
|
92
97
|
undef_method :find_by!
|
98
|
+
|
99
|
+
# @api private
|
100
|
+
def path_defaults
|
101
|
+
{
|
102
|
+
list: '/:path',
|
103
|
+
create: '/:path',
|
104
|
+
read: '/:path',
|
105
|
+
update: '/:path',
|
106
|
+
delete: '/:path'
|
107
|
+
}
|
108
|
+
end
|
93
109
|
end
|
94
110
|
end
|
95
111
|
end
|
data/lib/acfs/stub.rb
CHANGED
@@ -111,12 +111,35 @@ module Acfs
|
|
111
111
|
stub = stub_for op
|
112
112
|
unless stub
|
113
113
|
return false if allow_requests?
|
114
|
-
raise RealRequestsNotAllowedError
|
114
|
+
raise RealRequestsNotAllowedError.new <<-MSG.strip.gsub(/^[ ]{12}/, '')
|
115
|
+
No stub found for `#{op.action}' on `#{op.resource.name}' with params `#{op.full_params.inspect}', data `#{op.data.inspect}' and id `#{op.id}'.
|
116
|
+
|
117
|
+
Available stubs:
|
118
|
+
#{pretty_print}
|
119
|
+
MSG
|
115
120
|
end
|
116
121
|
|
117
122
|
stub.call op
|
118
123
|
true
|
119
124
|
end
|
125
|
+
|
126
|
+
private
|
127
|
+
def pretty_print
|
128
|
+
out = String.new
|
129
|
+
stubs.each do |klass, actions|
|
130
|
+
out << ' ' << klass.name << ":\n"
|
131
|
+
actions.each do |action, stubs|
|
132
|
+
stubs.each do |stub|
|
133
|
+
out << " #{action}"
|
134
|
+
out << " with #{stub.opts[:with].inspect}" if stub.opts[:with]
|
135
|
+
out << " and return #{stub.opts[:return].inspect}" if stub.opts[:return]
|
136
|
+
out << " and raise #{stub.opts[:raise].inspect}" if stub.opts[:raise]
|
137
|
+
out << "\n"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
out
|
142
|
+
end
|
120
143
|
end
|
121
144
|
end
|
122
145
|
end
|
@@ -3,7 +3,8 @@ require 'spec_helper'
|
|
3
3
|
describe Acfs::Model::Locatable do
|
4
4
|
let(:model) { MyUser }
|
5
5
|
before do
|
6
|
-
stub_request(:get,
|
6
|
+
stub_request(:get, 'http://users.example.org/users/1').to_return response({id: 1, name: 'Anon', age: 12})
|
7
|
+
stub_request(:get, 'http://users.example.org/users/1/profile').to_return response({user_id: 2, twitter_handle: '@anon'})
|
7
8
|
end
|
8
9
|
|
9
10
|
describe '.url' do
|
@@ -14,23 +15,43 @@ describe Acfs::Model::Locatable do
|
|
14
15
|
it 'should return URL with id path part if specified' do
|
15
16
|
expect(model.url(5)).to be == 'http://users.example.org/users/5'
|
16
17
|
end
|
18
|
+
|
19
|
+
context 'with attribute in path' do
|
20
|
+
let(:model) { Profile }
|
21
|
+
|
22
|
+
it 'should replace placeholder' do
|
23
|
+
expect(model.url(user_id: 1)).to eq 'http://users.example.org/users/1/profile'
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'without attributes' do
|
27
|
+
it 'should raise an error if attribute is missing' do
|
28
|
+
expect{ model.url }.to raise_error ArgumentError
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
17
32
|
end
|
18
33
|
|
19
34
|
describe '#url' do
|
20
35
|
context 'new resource' do
|
21
36
|
let(:m) { model.new }
|
22
37
|
|
23
|
-
it
|
38
|
+
it 'should return nil' do
|
24
39
|
expect(m.url).to be == nil
|
25
40
|
end
|
26
41
|
|
27
42
|
context 'new resource with id' do
|
28
43
|
let(:m) { model.new id: 475 }
|
29
44
|
|
30
|
-
it
|
45
|
+
it 'should return resource URL' do
|
31
46
|
expect(m.url).to be == 'http://users.example.org/users/475'
|
32
47
|
end
|
33
48
|
end
|
49
|
+
|
50
|
+
context 'with attribute in path' do
|
51
|
+
it 'should return nil' do
|
52
|
+
expect(m.url).to be == nil
|
53
|
+
end
|
54
|
+
end
|
34
55
|
end
|
35
56
|
|
36
57
|
context 'loaded resource' do
|
@@ -40,6 +61,15 @@ describe Acfs::Model::Locatable do
|
|
40
61
|
it "should return resource's URL" do
|
41
62
|
expect(m.url).to be == 'http://users.example.org/users/1'
|
42
63
|
end
|
64
|
+
|
65
|
+
context 'with attribute in path' do
|
66
|
+
let(:model) { Profile }
|
67
|
+
let(:m) { model.find user_id: 1 }
|
68
|
+
|
69
|
+
it "should return resource's URL" do
|
70
|
+
expect(m.url).to be == 'http://users.example.org/users/2/profile'
|
71
|
+
end
|
72
|
+
end
|
43
73
|
end
|
44
74
|
end
|
45
75
|
end
|
data/spec/acfs/service_spec.rb
CHANGED
@@ -17,16 +17,19 @@ describe Acfs::Service do
|
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
-
describe '#
|
20
|
+
describe '#location' do
|
21
|
+
let(:resource) { Class.new }
|
22
|
+
before { allow(resource).to receive(:path_defaults).and_return({}) }
|
23
|
+
|
21
24
|
it 'should extract resource path name from given class' do
|
22
|
-
expect(service.
|
25
|
+
expect(service.location(resource).to_s).to eq('/classes')
|
23
26
|
end
|
24
27
|
|
25
28
|
context 'with path options' do
|
26
29
|
let(:options) { { path: 'abc' } }
|
27
30
|
|
28
31
|
it 'should have custom resource path' do
|
29
|
-
expect(service.
|
32
|
+
expect(service.location(resource).to_s).to eq('/abc')
|
30
33
|
end
|
31
34
|
end
|
32
35
|
end
|
data/spec/support/service.rb
CHANGED
@@ -25,6 +25,13 @@ class MyUser < Acfs::Resource
|
|
25
25
|
attribute :age, :integer
|
26
26
|
end
|
27
27
|
|
28
|
+
class Profile < Acfs::SingletonResource
|
29
|
+
service UserService, path: 'users/:user_id/profile'
|
30
|
+
|
31
|
+
attribute :user_id, :integer
|
32
|
+
attribute :twitter_handle, :string
|
33
|
+
end
|
34
|
+
|
28
35
|
class Customer < MyUser
|
29
36
|
|
30
37
|
end
|
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.30.0.1.
|
4
|
+
version: 0.30.0.1.b261
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jan Graichen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-02-
|
11
|
+
date: 2014-02-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -126,6 +126,7 @@ files:
|
|
126
126
|
- lib/acfs/configuration.rb
|
127
127
|
- lib/acfs/errors.rb
|
128
128
|
- lib/acfs/global.rb
|
129
|
+
- lib/acfs/location.rb
|
129
130
|
- lib/acfs/middleware/base.rb
|
130
131
|
- lib/acfs/middleware/json_decoder.rb
|
131
132
|
- lib/acfs/middleware/json_encoder.rb
|