acfs 1.3.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -0
  3. data/README.md +3 -4
  4. data/acfs.gemspec +19 -10
  5. data/lib/acfs.rb +2 -0
  6. data/lib/acfs/adapter/base.rb +6 -8
  7. data/lib/acfs/adapter/typhoeus.rb +26 -7
  8. data/lib/acfs/collection.rb +2 -1
  9. data/lib/acfs/collections/paginatable.rb +2 -1
  10. data/lib/acfs/configuration.rb +14 -7
  11. data/lib/acfs/errors.rb +33 -12
  12. data/lib/acfs/global.rb +12 -2
  13. data/lib/acfs/location.rb +9 -5
  14. data/lib/acfs/middleware/base.rb +5 -1
  15. data/lib/acfs/middleware/json.rb +2 -0
  16. data/lib/acfs/middleware/logger.rb +2 -0
  17. data/lib/acfs/middleware/msgpack.rb +2 -0
  18. data/lib/acfs/middleware/print.rb +2 -0
  19. data/lib/acfs/middleware/serializer.rb +2 -0
  20. data/lib/acfs/operation.rb +5 -3
  21. data/lib/acfs/request.rb +3 -0
  22. data/lib/acfs/request/callbacks.rb +5 -1
  23. data/lib/acfs/resource.rb +2 -0
  24. data/lib/acfs/resource/attributes.rb +3 -2
  25. data/lib/acfs/resource/attributes/base.rb +2 -1
  26. data/lib/acfs/resource/attributes/boolean.rb +2 -0
  27. data/lib/acfs/resource/attributes/date_time.rb +2 -1
  28. data/lib/acfs/resource/attributes/dict.rb +2 -0
  29. data/lib/acfs/resource/attributes/float.rb +5 -3
  30. data/lib/acfs/resource/attributes/integer.rb +2 -0
  31. data/lib/acfs/resource/attributes/list.rb +2 -0
  32. data/lib/acfs/resource/attributes/string.rb +2 -0
  33. data/lib/acfs/resource/attributes/uuid.rb +4 -3
  34. data/lib/acfs/resource/dirty.rb +2 -0
  35. data/lib/acfs/resource/initialization.rb +2 -0
  36. data/lib/acfs/resource/loadable.rb +2 -0
  37. data/lib/acfs/resource/locatable.rb +10 -6
  38. data/lib/acfs/resource/operational.rb +2 -1
  39. data/lib/acfs/resource/persistence.rb +7 -4
  40. data/lib/acfs/resource/query_methods.rb +5 -3
  41. data/lib/acfs/resource/service.rb +3 -1
  42. data/lib/acfs/resource/validation.rb +3 -1
  43. data/lib/acfs/response.rb +2 -0
  44. data/lib/acfs/response/formats.rb +2 -0
  45. data/lib/acfs/response/status.rb +3 -1
  46. data/lib/acfs/rspec.rb +2 -0
  47. data/lib/acfs/runner.rb +6 -1
  48. data/lib/acfs/service.rb +8 -2
  49. data/lib/acfs/service/middleware.rb +2 -0
  50. data/lib/acfs/service/middleware/stack.rb +5 -3
  51. data/lib/acfs/singleton_resource.rb +4 -2
  52. data/lib/acfs/stub.rb +33 -11
  53. data/lib/acfs/util.rb +2 -0
  54. data/lib/acfs/version.rb +3 -1
  55. data/lib/acfs/yard.rb +1 -0
  56. data/spec/acfs/adapter/typhoeus_spec.rb +30 -3
  57. data/spec/acfs/collection_spec.rb +7 -5
  58. data/spec/acfs/configuration_spec.rb +2 -0
  59. data/spec/acfs/global_spec.rb +48 -1
  60. data/spec/acfs/location_spec.rb +25 -0
  61. data/spec/acfs/middleware/json_spec.rb +2 -0
  62. data/spec/acfs/middleware/msgpack_spec.rb +2 -0
  63. data/spec/acfs/operation_spec.rb +2 -0
  64. data/spec/acfs/request/callbacks_spec.rb +2 -0
  65. data/spec/acfs/request_spec.rb +3 -1
  66. data/spec/acfs/resource/attributes/boolean_spec.rb +2 -0
  67. data/spec/acfs/resource/attributes/date_time_spec.rb +2 -0
  68. data/spec/acfs/resource/attributes/dict_spec.rb +4 -2
  69. data/spec/acfs/resource/attributes/float_spec.rb +2 -0
  70. data/spec/acfs/resource/attributes/integer_spec.rb +2 -0
  71. data/spec/acfs/resource/attributes/list_spec.rb +5 -3
  72. data/spec/acfs/resource/attributes/uuid_spec.rb +2 -0
  73. data/spec/acfs/resource/attributes_spec.rb +6 -4
  74. data/spec/acfs/resource/dirty_spec.rb +2 -0
  75. data/spec/acfs/resource/initialization_spec.rb +8 -2
  76. data/spec/acfs/resource/loadable_spec.rb +2 -0
  77. data/spec/acfs/resource/locatable_spec.rb +2 -0
  78. data/spec/acfs/resource/persistance_spec.rb +10 -4
  79. data/spec/acfs/resource/query_methods_spec.rb +25 -18
  80. data/spec/acfs/resource/validation_spec.rb +2 -0
  81. data/spec/acfs/response/formats_spec.rb +3 -1
  82. data/spec/acfs/response/status_spec.rb +2 -0
  83. data/spec/acfs/runner_spec.rb +6 -8
  84. data/spec/acfs/service/middleware_spec.rb +2 -0
  85. data/spec/acfs/service_spec.rb +3 -1
  86. data/spec/acfs/singleton_resource_spec.rb +2 -0
  87. data/spec/acfs/stub_spec.rb +2 -0
  88. data/spec/acfs_spec.rb +2 -0
  89. data/spec/spec_helper.rb +11 -6
  90. data/spec/support/hash.rb +2 -0
  91. data/spec/support/response.rb +2 -0
  92. data/spec/support/service.rb +1 -0
  93. data/spec/support/shared/find_callbacks.rb +2 -0
  94. metadata +12 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 81eada64bf268a7ee4bdec316baf65e2f8ca0ac8cd80120474dfede36d4b4f51
4
- data.tar.gz: b5a0d11f1eb60c8bfb16ccd66a8d7ee8f09fcaeea8976f47540239725a08d08b
3
+ metadata.gz: 6f998ff3e28801676c732fa44df93e18f651e96eb986c857c61b7f25b312a2c1
4
+ data.tar.gz: a122064cb630a664489629ef658d5ecde917ced4cdc43a600b49d0e18e00bea7
5
5
  SHA512:
6
- metadata.gz: 2cb0a9482b37b1a57bfad86f4213040d27babbf2cd6d4496a93b3260e50435ff1c575d57ba4a0d48b22ada2a3fbda44289283da1032fe3ecdc6010fe39f7646e
7
- data.tar.gz: b21823474d56cdda6ca816f21d3f76674aa00a4d2636ec0dedc15deecd17469ef55716688306a654952bee4438b5ce338cc2471b4db644852daaa09e0b2bbc4c
6
+ metadata.gz: 7801c96d794f08ad03b9ad204f8e7c8b611300922b12714ce7864a33c5a6268bc47c42e66b86c410358f17a0439d382c358ba15283eee07ff092d19a1f86a8be
7
+ data.tar.gz: 78b2c804251fb6aaee270d9c894ff36b7b8dc7bb777148de55963255847fc28adcdbff9c7132352c5448bef7b9980fd72e6a14ffceda38540d661aceaf1e5e9d
@@ -1,5 +1,55 @@
1
1
  # Changelog
2
2
 
3
+
4
+
5
+ ## Unreleased
6
+ ---
7
+
8
+ ### New
9
+
10
+ ### Changes
11
+
12
+ ### Fixes
13
+
14
+ ### Breaks
15
+
16
+
17
+ ## 1.4.0 - (2020-06-12)
18
+ ---
19
+
20
+ ### New
21
+ * Use strict TCP keepalive probing by default (5s/5s)
22
+ * Adapter accepts curl request opts
23
+
24
+
25
+ ## 1.3.4 - (2020-03-22)
26
+ ---
27
+
28
+ ### Fixes
29
+ * Empty package build for Gem release 1.3.3
30
+
31
+
32
+ ## 1.3.3 - (2020-03-22)
33
+ ---
34
+
35
+ ### Changes
36
+ * Improved handling of low-level connection errors and timeouts
37
+
38
+
39
+ ## 1.3.2 - (2019-09-24)
40
+
41
+
42
+ ### Fixes
43
+ * Fix Acfs.on callbacks for empty find_by results (#42)
44
+
45
+
46
+ ---
47
+
48
+ ## 1.3.1 - (2019-07-02)
49
+
50
+ ### Fixes
51
+ * Improve URL argument encoding when building resource requests
52
+
3
53
  ## 1.3.0
4
54
 
5
55
  * Change default error messages to a more compact representation to ease integration with error reporting services.
data/README.md CHANGED
@@ -1,10 +1,9 @@
1
1
  # Acfs - *API client for services*
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/acfs.svg)](http://badge.fury.io/rb/acfs)
4
- [![Build Status](http://img.shields.io/travis/jgraichen/acfs/master.svg)](https://travis-ci.org/jgraichen/acfs)
3
+ [![Gem Version](https://img.shields.io/gem/v/acfs?logo=ruby)](https://rubygems.org/gems/acfs)
4
+ [![Build Status](https://img.shields.io/travis/jgraichen/acfs/master?logo=travis)](https://travis-ci.org/jgraichen/acfs)
5
+ [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/jgraichen/acfs/Test/master?logo=github)](https://github.com/jgraichen/acfs/actions?query=branch%3Amaster)
5
6
  [![Coverage Status](http://img.shields.io/coveralls/jgraichen/acfs/master.svg)](https://coveralls.io/r/jgraichen/acfs)
6
- [![Code Climate](http://img.shields.io/codeclimate/github/jgraichen/acfs.svg)](https://codeclimate.com/github/jgraichen/acfs)
7
- [![Dependency Status](http://img.shields.io/gemnasium/jgraichen/acfs.svg)](https://gemnasium.com/jgraichen/acfs)
8
7
  [![RubyDoc Documentation](http://img.shields.io/badge/rubydoc-here-blue.svg)](http://rubydoc.info/github/jgraichen/acfs/master/frames)
9
8
 
10
9
  Acfs is a library to develop API client libraries for single services within a larger service oriented application.
@@ -1,5 +1,6 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require 'acfs/version'
5
6
 
@@ -7,20 +8,26 @@ Gem::Specification.new do |spec|
7
8
  spec.name = 'acfs'
8
9
  spec.version = Acfs::VERSION
9
10
  spec.authors = ['Jan Graichen']
10
- spec.email = %w(jg@altimos.de)
11
- spec.description = 'API Client For Services'
12
- spec.summary = 'An abstract API base client for service oriented application.'
11
+ spec.email = %w[jgraichen@altimos.de]
13
12
  spec.homepage = 'https://github.com/jgraichen/acfs'
14
13
  spec.license = 'MIT'
14
+ spec.description = 'API Client For Services'
15
+ spec.summary = <<~SUMMARY.strip
16
+ An abstract API base client for service oriented application.
17
+ SUMMARY
18
+
19
+ spec.files = Dir['**/*'].grep(%r{
20
+ ^((bin|lib|test|spec|features)/|
21
+ .*\.gemspec|.*LICENSE.*|.*README.*|.*CHANGELOG.*)
22
+ }xi)
15
23
 
16
- spec.files = Dir['**/*'].grep(%r{^((bin|lib|test|spec|features)/|.*\.gemspec|.*LICENSE.*|.*README.*|.*CHANGELOG.*)})
17
24
  spec.executables = spec.files.grep(%r{^bin/}) {|f| File.basename(f) }
18
25
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = %w(lib)
26
+ spec.require_paths = %w[lib]
20
27
 
21
- spec.add_runtime_dependency 'activesupport', '>= 4.2'
22
- spec.add_runtime_dependency 'activemodel', '>= 4.2'
23
28
  spec.add_runtime_dependency 'actionpack', '>= 4.2'
29
+ spec.add_runtime_dependency 'activemodel', '>= 4.2'
30
+ spec.add_runtime_dependency 'activesupport', '>= 4.2'
24
31
  spec.add_runtime_dependency 'multi_json'
25
32
 
26
33
  # Bundle update w/o version resolves to 0.3.3 ...
@@ -28,10 +35,12 @@ Gem::Specification.new do |spec|
28
35
 
29
36
  spec.add_runtime_dependency 'rack'
30
37
 
31
- spec.add_development_dependency 'bundler', '~> 1.3'
38
+ spec.add_development_dependency 'bundler'
32
39
 
33
40
  if ENV['TRAVIS_BUILD_NUMBER'] && !ENV['TRAVIS_TAG']
34
41
  # Append travis build number for auto-releases
42
+ # rubocop:disable Gemspec/DuplicatedAssignment
35
43
  spec.version = "#{spec.version}.1.b#{ENV['TRAVIS_BUILD_NUMBER']}"
44
+ # rubocop:enable Gemspec/DuplicatedAssignment
36
45
  end
37
46
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support'
2
4
  require 'active_support/core_ext/hash'
3
5
  require 'active_support/core_ext/class'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Acfs::Adapter
2
4
  # Base adapter handling operation queuing
3
5
  # and processing.
@@ -5,22 +7,18 @@ module Acfs::Adapter
5
7
  class Base
6
8
  # Start processing queued requests.
7
9
  #
8
- def start
9
- end
10
+ def start; end
10
11
 
11
12
  # Abort running and queued requests.
12
13
  #
13
- def abort
14
- end
14
+ def abort; end
15
15
 
16
16
  # Run request right now skipping queue.
17
17
  #
18
- def run(_)
19
- end
18
+ def run(_); end
20
19
 
21
20
  # Enqueue request to be run later.
22
21
  #
23
- def queue(_)
24
- end
22
+ def queue(_); end
25
23
  end
26
24
  end
@@ -1,17 +1,26 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'typhoeus'
2
4
 
3
5
  module Acfs
4
6
  module Adapter
7
+ DEFAULT_OPTIONS = {
8
+ tcp_keepalive: true,
9
+ tcp_keepidle: 5,
10
+ tcp_keepintvl: 5
11
+ }.freeze
12
+
5
13
  # Adapter for Typhoeus.
6
14
  #
7
15
  class Typhoeus < Base
8
- def initialize(**kwargs)
9
- @options = kwargs
16
+ def initialize(opts: {}, **kwargs)
17
+ @opts = DEFAULT_OPTIONS.merge(opts)
18
+ @kwargs = kwargs
10
19
  end
11
20
 
12
21
  def start
13
22
  hydra.run
14
- rescue
23
+ rescue StandardError
15
24
  @hydra = nil
16
25
  raise
17
26
  end
@@ -29,21 +38,31 @@ module Acfs
29
38
  protected
30
39
 
31
40
  def hydra
32
- @hydra ||= ::Typhoeus::Hydra.new(**@options)
41
+ @hydra ||= ::Typhoeus::Hydra.new(**@kwargs)
33
42
  end
34
43
 
35
44
  def convert_request(req)
36
- request = ::Typhoeus::Request.new req.url,
45
+ opts = {
37
46
  method: req.method,
38
47
  params: req.params,
39
48
  headers: req.headers.merge(
40
- 'Expect' => '',
49
+ 'Expect' => '',
41
50
  'Transfer-Encoding' => ''
42
51
  ),
43
52
  body: req.body
53
+ }
54
+
55
+ request = ::Typhoeus::Request.new(req.url, **@opts.merge(opts))
44
56
 
45
57
  request.on_complete do |response|
46
- req.complete! convert_response(req, response)
58
+ if response.timed_out?
59
+ raise ::Acfs::TimeoutError.new(req)
60
+ elsif response.code.zero?
61
+ # Failed to get HTTP response
62
+ raise ::Acfs::RequestError.new(req, response.return_message)
63
+ else
64
+ req.complete! convert_response(req, response)
65
+ end
47
66
  end
48
67
 
49
68
  request
@@ -1,10 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'delegate'
2
4
 
3
5
  require 'acfs/resource/loadable'
4
6
  require 'acfs/collections/paginatable'
5
7
 
6
8
  module Acfs
7
- #
8
9
  class Collection < ::Delegator
9
10
  include Resource::Loadable
10
11
  include Acfs::Util::Callbacks
@@ -1,5 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Acfs::Collections
2
- #
3
4
  module Paginatable
4
5
  extend ActiveSupport::Concern
5
6
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'uri'
2
4
  require 'yaml'
3
5
 
@@ -25,7 +27,7 @@ module Acfs
25
27
  # @return [undefined]
26
28
  #
27
29
  def configure(&block)
28
- if block.arity > 0
30
+ if block.arity.positive?
29
31
  block.call self
30
32
  else
31
33
  instance_eval(&block)
@@ -37,8 +39,12 @@ module Acfs
37
39
  # @overload locate(service, uri)
38
40
  # Configures URL where a service can be reached.
39
41
  #
40
- # @param [Symbol] service Service identity key for service that is reachable under given URL.
41
- # @param [String] uri URL where service is reachable. Will be passed to {URI.parse}.
42
+ # @param [Symbol] service
43
+ # Service identity key for service that is reachable under given URL.
44
+ #
45
+ # @param [String] uri
46
+ # URL where service is reachable. Will be passed to {URI.parse}.
47
+ #
42
48
  # @return [undefined]
43
49
  #
44
50
  # @overload locate(service)
@@ -64,7 +70,7 @@ module Acfs
64
70
  # @return [undefined]
65
71
  #
66
72
  def load(filename)
67
- config = YAML.load File.read filename
73
+ config = YAML.safe_load(File.read(filename), [], [], true)
68
74
  env = ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development'
69
75
 
70
76
  config = config[env] if config.key? env
@@ -95,18 +101,19 @@ module Acfs
95
101
  # @return [Configuration]
96
102
  #
97
103
  def current
98
- @configuration ||= new
104
+ @current ||= new
99
105
  end
100
106
 
101
107
  # @api private
102
108
  #
103
- # Swap configuration object with given new one. Must be a {Configuration} object.
109
+ # Swap configuration object with given new one. Must be
110
+ # a {Configuration} object.
104
111
  #
105
112
  # @param [Configuration] configuration
106
113
  # @return [undefined]
107
114
  #
108
115
  def set(configuration)
109
- @configuration = configuration if configuration.is_a? Configuration
116
+ @current = configuration if configuration.is_a? Configuration
110
117
  end
111
118
  end
112
119
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Acfs
2
4
  # Acfs base error.
3
5
  #
@@ -10,6 +12,24 @@ module Acfs
10
12
 
11
13
  class UnsupportedOperation < StandardError; end
12
14
 
15
+ class RequestError < Error
16
+ attr_reader :request
17
+
18
+ def initialize(request, message)
19
+ @request = request
20
+
21
+ message = "#{message}: #{request.method.upcase} #{request.url}"
22
+
23
+ super message: message
24
+ end
25
+ end
26
+
27
+ class TimeoutError < RequestError
28
+ def initialize(request)
29
+ super(request, "Timeout reached")
30
+ end
31
+ end
32
+
13
33
  # Response error containing the responsible response object.
14
34
  #
15
35
  class ErroneousResponse < Error
@@ -17,17 +37,15 @@ module Acfs
17
37
 
18
38
  def initialize(opts = {})
19
39
  @response = opts[:response]
20
- message = opts[:message]
40
+
21
41
  if response
22
- if message
23
- message << ':'
24
- else
25
- message = 'Received'
26
- end
27
- message << " #{response.code} for #{response.request.method.upcase} #{response.request.url} #{response.request.format}"
42
+ message = (opts[:message] ? opts[:message] + ':' : 'Received') +
43
+ " #{response.code} for #{response.request.method.upcase}" \
44
+ " #{response.request.url} #{response.request.format}"
28
45
  else
29
- message ||= 'Received erroneous response'
46
+ message = opts[:message] || 'Received erroneous response'
30
47
  end
48
+
31
49
  super opts, message
32
50
  end
33
51
  end
@@ -41,8 +59,11 @@ module Acfs
41
59
  @stubs = opts.delete :stubs
42
60
  @operation = opts.delete :operation
43
61
 
44
- super opts, "Ambiguous stubs for #{operation.action} on #{operation.resource}.\n" +
45
- stubs.map {|s| " #{s.opts.pretty_inspect}" }.join
62
+ message = "Ambiguous stubs for #{operation.action} " \
63
+ "on #{operation.resource}.\n" +
64
+ stubs.map {|s| " #{s.opts.pretty_inspect}" }.join
65
+
66
+ super opts, message
46
67
  end
47
68
  end
48
69
 
@@ -51,7 +72,6 @@ module Acfs
51
72
  class ResourceNotFound < ErroneousResponse
52
73
  end
53
74
 
54
- #
55
75
  class InvalidResource < ErroneousResponse
56
76
  attr_reader :errors, :resource
57
77
 
@@ -97,7 +117,8 @@ module Acfs
97
117
  def initialize(opts = {})
98
118
  @base_class = opts.delete :base_class
99
119
  @type_name = opts.delete :type_name
100
- opts[:message] = "Received resource type `#{type_name}` is no subclass of #{base_class}"
120
+ opts[:message] = "Received resource type `#{type_name}` " \
121
+ "is no subclass of #{base_class}"
101
122
  super
102
123
  end
103
124
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Acfs
2
4
  #
3
5
  # Global Acfs module methods.
@@ -73,7 +75,7 @@ module Acfs
73
75
  end
74
76
  return false if block.nil?
75
77
 
76
- if resource.loaded?
78
+ if resource.nil? || resource.loaded?
77
79
  block.call resource
78
80
  else
79
81
  resource.__callbacks__ << block
@@ -81,9 +83,17 @@ module Acfs
81
83
  end
82
84
 
83
85
  def on(*resources)
86
+ # If all resources have already been loaded, we run the callback immediately.
87
+ if resources.all? {|res| res.nil? || res.loaded? }
88
+ yield(*resources)
89
+ return
90
+ end
91
+
92
+ # Otherwise, we add a callback to *each* resource with a guard that ensures
93
+ # that only the very last resource being loaded executes the callback.
84
94
  resources.each do |resource|
85
95
  add_callback resource do |_|
86
- yield(*resources) unless resources.any? {|res| !res.loaded? }
96
+ yield(*resources) if resources.all? {|res| res.nil? || res.loaded? }
87
97
  end
88
98
  end
89
99
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Acfs
2
4
  # @api private
3
5
  #
@@ -6,7 +8,7 @@ module Acfs
6
8
  class Location
7
9
  attr_reader :arguments, :raw, :struct, :args
8
10
 
9
- REGEXP = /^:([A-z][A-z0-9_]*)$/
11
+ REGEXP = /^:([A-z][A-z0-9_]*)$/.freeze
10
12
 
11
13
  def initialize(uri, args = {})
12
14
  @raw = URI.parse uri
@@ -33,20 +35,22 @@ module Acfs
33
35
 
34
36
  def str
35
37
  uri = raw.dup
36
- uri.path = URI.escape '/' + struct.map {|s| lookup_arg(s, args) }.join('/')
38
+ uri.path = '/' + struct.map {|s| lookup_arg(s, args) }.join('/')
37
39
  uri.to_s
38
40
  end
39
41
 
40
42
  def raw_uri
41
43
  raw.to_s
42
44
  end
43
- alias_method :to_s, :raw_uri
45
+ alias to_s raw_uri
44
46
 
45
47
  private
46
48
 
47
49
  def extract_arg(key, hashes)
48
50
  hashes.each_with_index do |hash, index|
49
- return (index == 0 ? hash.delete(key) : hash.fetch(key)) if hash.key?(key)
51
+ if hash.key?(key)
52
+ return (index == 0 ? hash.delete(key) : hash.fetch(key))
53
+ end
50
54
  end
51
55
 
52
56
  nil
@@ -58,7 +62,7 @@ module Acfs
58
62
 
59
63
  def lookup_replacement(sym, args)
60
64
  value = get_replacement(sym, args).to_s
61
- return value unless value.empty?
65
+ return ::URI.encode_www_form_component(value) unless value.empty?
62
66
 
63
67
  raise ArgumentError.new "Cannot replace path argument `#{sym}' with empty string."
64
68
  end