acfs 0.30.0.1.b260 → 0.30.0.1.b261
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 +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
|