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 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