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 CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- YzFkODNlZWExZDEzODUwYTIyOTQ4NGM1ZmUwYjI0NzhjYzlhYmZmNg==
4
+ MDBhNGMzOTliN2YzYWNiZjZiMmM5ZjZhODMxYzk5NmU4MDU2ODA2Mg==
5
5
  data.tar.gz: !binary |-
6
- Y2EyMGRlNTNjMGQxODBkNDVhZmQwOWUwMmZkMjJkOWIyMjY1OTUzMA==
6
+ NTAyZDVkZDEzOWFhY2I1OTM0OTQwMjk5MmE4YzIyZTQ4ZmJhNmY2MA==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- MWU5OTNjYWY1YTMxOWE5ODVlMzBhYzg1ZDgzYzcyN2I5YjkwMDBmNGMxOTNk
10
- MTczZDFlOThjODAyMWQzMWY3M2RkYmYyOThkNmVjMDU1NDc5MGZmMjk2ZmE4
11
- N2ZiZTQ3NjczMTgzZmZjNDFiMDNiZDVkZTUzMGQzODNlOTg2MDY=
9
+ N2M4YTAwOTZmMWNkY2E1NWVjYzcxNjFlY2QwN2MwMGU5YmM5ZWEwNjA0MTEy
10
+ OTY4OWZhZjliZjI1ZGM3ZWY2OGE1ZjUwNjc5NjUyMTI1OWI2NDUyODgyNDg1
11
+ MmU3ODYxMmZhNTAyMWUxOGFiZjA2YzRjMWNkNjgxNmU2YWIxNjY=
12
12
  data.tar.gz: !binary |-
13
- YjFjZWJlYzlmNjQ2M2E2ZTlkZjlhMWE1NmExZDE3M2UyNDJhMjk3ZDk1ZjNl
14
- NmZiMDgyNTBiZDZmNjY1ZWM2YzkzZDc1ZmJmYjc0ZGM4NDg1YzliMGQ2YWU2
15
- MTljMjg1ZDAwYmE2NWJlNmI1NDE3ZTZkZWEyYmE1Yzk3ODA3N2Q=
13
+ MmEyYWRlODQyODNlNWNkNTQ2NmFjZjhiNDliNTc1ZTA5M2QwMWRiNTljOTUz
14
+ YjBmYmQzMGQ1M2Q3ZjYyZWUxM2U5OTAwMDIwZDZhMWU3Y2QyMmRmYzMxMmJl
15
+ MDdmOTRjMmJmYjRiMjUxYmY4YzI1YmZmNDhhMjRhNmNkMTczN2M=
@@ -13,6 +13,7 @@ module Acfs
13
13
 
14
14
  require 'acfs/collection'
15
15
  require 'acfs/configuration'
16
+ require 'acfs/location'
16
17
  require 'acfs/model'
17
18
  require 'acfs/operation'
18
19
  require 'acfs/request'
@@ -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
@@ -14,18 +14,83 @@ module Acfs::Model
14
14
 
15
15
  module ClassMethods
16
16
 
17
- # Return URL for this class of resource. Given suffix will be appended.
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
- # User.url # => "http://users.srv.org/users"
21
- # User.url(5) # => "http://users.srv.org/users/5"
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
- # @param [ String ] suffix Suffix to append to URL.
24
- # @return [ String ] Generated URL.
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
- def url(suffix = nil)
28
- service.url_for(self, suffix: suffix)
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 id.nil?
47
- self.class.service.url_for self, suffix: read_attribute(:id)
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
@@ -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 url, method: method, params: params, data: data
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
@@ -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) || {}
@@ -32,15 +32,18 @@ module Acfs
32
32
  end
33
33
 
34
34
  # @api private
35
- # @return [String]
35
+ # @return [Location]
36
36
  #
37
- def url_for(resource_class, options = {})
38
- options.reverse_merge! self.options
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 += "/#{(options[:path] || resource_class.name.pluralize.underscore).to_s}"
42
- url += "/#{options[:suffix].to_s}" if options[:suffix]
43
- url
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
@@ -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, "No stub found for #{op.action} on #{op.resource.name} with params #{op.full_params.inspect}, data #{op.data.inspect} and id #{op.id}."
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, "http://users.example.org/users/1").to_return response({ id: 1, name: "Anon", age: 12 })
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 "should return nil" do
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 "should return resource URL" do
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
@@ -17,16 +17,19 @@ describe Acfs::Service do
17
17
  end
18
18
  end
19
19
 
20
- describe '#url_for' do
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.url_for(Class)).to eq('/classes')
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.url_for(Class)).to eq('/abc')
32
+ expect(service.location(resource).to_s).to eq('/abc')
30
33
  end
31
34
  end
32
35
  end
@@ -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.b260
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-07 00:00:00.000000000 Z
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