smithy-client 1.0.0.pre0 → 1.0.0.pre1

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.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +2 -0
  3. data/VERSION +1 -1
  4. data/lib/smithy-client/anonymous_provider.rb +12 -0
  5. data/lib/smithy-client/auth_option.rb +23 -0
  6. data/lib/smithy-client/auth_scheme.rb +25 -0
  7. data/lib/smithy-client/auth_schemes/anonymous.rb +18 -0
  8. data/lib/smithy-client/auth_schemes/http_api_key.rb +18 -0
  9. data/lib/smithy-client/auth_schemes/http_basic.rb +18 -0
  10. data/lib/smithy-client/auth_schemes/http_bearer.rb +18 -0
  11. data/lib/smithy-client/auth_schemes/http_digest.rb +18 -0
  12. data/lib/smithy-client/base.rb +200 -0
  13. data/lib/smithy-client/block_io.rb +36 -0
  14. data/lib/smithy-client/configuration.rb +222 -0
  15. data/lib/smithy-client/default_params.rb +91 -0
  16. data/lib/smithy-client/dynamic_errors.rb +82 -0
  17. data/lib/smithy-client/endpoint_rules.rb +186 -0
  18. data/lib/smithy-client/handler.rb +29 -0
  19. data/lib/smithy-client/handler_builder.rb +33 -0
  20. data/lib/smithy-client/handler_context.rb +67 -0
  21. data/lib/smithy-client/handler_list.rb +197 -0
  22. data/lib/smithy-client/handler_list_entry.rb +102 -0
  23. data/lib/smithy-client/http/error_inspector.rb +87 -0
  24. data/lib/smithy-client/http/headers.rb +122 -0
  25. data/lib/smithy-client/http/request.rb +57 -0
  26. data/lib/smithy-client/http/response.rb +178 -0
  27. data/lib/smithy-client/http_api_key_provider.rb +18 -0
  28. data/lib/smithy-client/http_bearer_provider.rb +18 -0
  29. data/lib/smithy-client/http_login_provider.rb +19 -0
  30. data/lib/smithy-client/identities/anonymous.rb +10 -0
  31. data/lib/smithy-client/identities/http_api_key.rb +18 -0
  32. data/lib/smithy-client/identities/http_bearer.rb +18 -0
  33. data/lib/smithy-client/identities/http_login.rb +22 -0
  34. data/lib/smithy-client/identity.rb +15 -0
  35. data/lib/smithy-client/log_formatter.rb +215 -0
  36. data/lib/smithy-client/log_param_filter.rb +88 -0
  37. data/lib/smithy-client/log_param_formatter.rb +65 -0
  38. data/lib/smithy-client/managed_file.rb +14 -0
  39. data/lib/smithy-client/net_http/connection_pool.rb +297 -0
  40. data/lib/smithy-client/net_http/handler.rb +160 -0
  41. data/lib/smithy-client/net_http/patches.rb +28 -0
  42. data/lib/smithy-client/networking_error.rb +16 -0
  43. data/lib/smithy-client/pageable_response.rb +138 -0
  44. data/lib/smithy-client/param_converter.rb +243 -0
  45. data/lib/smithy-client/param_validator.rb +213 -0
  46. data/lib/smithy-client/plugin.rb +144 -0
  47. data/lib/smithy-client/plugin_list.rb +141 -0
  48. data/lib/smithy-client/plugins/anonymous_auth.rb +23 -0
  49. data/lib/smithy-client/plugins/checksum_required.rb +51 -0
  50. data/lib/smithy-client/plugins/content_length.rb +26 -0
  51. data/lib/smithy-client/plugins/default_params.rb +22 -0
  52. data/lib/smithy-client/plugins/host_prefix.rb +69 -0
  53. data/lib/smithy-client/plugins/http_api_key_auth.rb +37 -0
  54. data/lib/smithy-client/plugins/http_basic_auth.rb +47 -0
  55. data/lib/smithy-client/plugins/http_bearer_auth.rb +37 -0
  56. data/lib/smithy-client/plugins/http_digest_auth.rb +60 -0
  57. data/lib/smithy-client/plugins/idempotency_token.rb +34 -0
  58. data/lib/smithy-client/plugins/logging.rb +56 -0
  59. data/lib/smithy-client/plugins/net_http.rb +163 -0
  60. data/lib/smithy-client/plugins/pageable_response.rb +37 -0
  61. data/lib/smithy-client/plugins/param_converter.rb +32 -0
  62. data/lib/smithy-client/plugins/param_validator.rb +30 -0
  63. data/lib/smithy-client/plugins/protocol.rb +66 -0
  64. data/lib/smithy-client/plugins/raise_response_errors.rb +33 -0
  65. data/lib/smithy-client/plugins/request_compression.rb +200 -0
  66. data/lib/smithy-client/plugins/response_target.rb +71 -0
  67. data/lib/smithy-client/plugins/retry_errors.rb +125 -0
  68. data/lib/smithy-client/plugins/sign_requests.rb +24 -0
  69. data/lib/smithy-client/plugins/stub_responses.rb +102 -0
  70. data/lib/smithy-client/protocol_spec_matcher.rb +60 -0
  71. data/lib/smithy-client/refreshing_identity_provider.rb +65 -0
  72. data/lib/smithy-client/request.rb +76 -0
  73. data/lib/smithy-client/response.rb +48 -0
  74. data/lib/smithy-client/retry/adaptive.rb +66 -0
  75. data/lib/smithy-client/retry/client_rate_limiter.rb +142 -0
  76. data/lib/smithy-client/retry/quota.rb +58 -0
  77. data/lib/smithy-client/retry/standard.rb +52 -0
  78. data/lib/smithy-client/retry.rb +36 -0
  79. data/lib/smithy-client/rpc_v2_cbor/protocol.rb +38 -0
  80. data/lib/smithy-client/rpc_v2_cbor/request_builder.rb +76 -0
  81. data/lib/smithy-client/rpc_v2_cbor/response_parser.rb +86 -0
  82. data/lib/smithy-client/rpc_v2_cbor/response_stubber.rb +34 -0
  83. data/lib/smithy-client/service_error.rb +57 -0
  84. data/lib/smithy-client/signer.rb +16 -0
  85. data/lib/smithy-client/signers/anonymous.rb +13 -0
  86. data/lib/smithy-client/signers/http_api_key.rb +52 -0
  87. data/lib/smithy-client/signers/http_basic.rb +23 -0
  88. data/lib/smithy-client/signers/http_bearer.rb +19 -0
  89. data/lib/smithy-client/signers/http_digest.rb +21 -0
  90. data/lib/smithy-client/stubbing/data_applicator.rb +61 -0
  91. data/lib/smithy-client/stubbing/empty_stub.rb +69 -0
  92. data/lib/smithy-client/stubbing/endpoint_provider.rb +22 -0
  93. data/lib/smithy-client/stubbing/protocol.rb +29 -0
  94. data/lib/smithy-client/stubbing/stub_data.rb +25 -0
  95. data/lib/smithy-client/stubbing.rb +14 -0
  96. data/lib/smithy-client/stubs.rb +212 -0
  97. data/lib/smithy-client/util.rb +15 -0
  98. data/lib/smithy-client/waiters/poller.rb +93 -0
  99. data/lib/smithy-client/waiters/waiter.rb +113 -0
  100. data/lib/smithy-client.rb +66 -1
  101. metadata +163 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dbcc7c50be767042e3eae29206ceb174f523f1d5421c4ec90d0457c24acd9b10
4
- data.tar.gz: 5ef14301b0132b7018a9782f01b2a6b4a8c62fbcb6f74a614bebb19fe16ea86d
3
+ metadata.gz: fe9f255b34cb45c0052d24be6f0f8415d129311b369e3a801b2a8945bb43ef47
4
+ data.tar.gz: 6ee595a5adb44533dddc82fa08e80d40aa944ec82dfd76b71216c556d54d0396
5
5
  SHA512:
6
- metadata.gz: 4a6471db9b9a88bdc41546e9b004e6e7871611ba00eeb499cfbee9d28f750aff842a360786a2111add5bfc57c009fef074f494900f814cf0eb6d3c7c6a4f7d43
7
- data.tar.gz: 184ac4c3e36023db7b7212aecf825535d1b5bae6701f3c63a2ebf5afdf5ddd5e1a5d15e34db8a314144a9a296fa6ce7298899514849a47d051b5b595da1bc8f8
6
+ metadata.gz: b8b6968ff630c4178da4f630fe3977a7112a88b84d6aeecccc515ed68566f4dbcb9ed1b0a0ec4bfa03a2f6d9b3430962d488327e902d597d58be8b569645302a
7
+ data.tar.gz: '0298dcc75712c68d05ce931f582e0873340e7059d7a238dfbc6f14877868040cf6401b7bb75c1874d4b1e24feb5f37468d20a80e7b8ae18a133ca81476c29f9c'
data/CHANGELOG.md CHANGED
@@ -1,2 +1,4 @@
1
1
  Unreleased Changes
2
2
  ------------------
3
+
4
+ * Feature - Initial version of this gem.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.0.pre0
1
+ 1.0.0.pre1
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smithy
4
+ module Client
5
+ # @api private
6
+ class AnonymousProvider
7
+ def identity(_properties)
8
+ Identities::Anonymous.new
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smithy
4
+ module Client
5
+ # Object that represents an auth option, returned by Auth Resolvers.
6
+ class AuthOption
7
+ def initialize(scheme_id:, identity_properties: {}, signer_properties: {})
8
+ @scheme_id = scheme_id
9
+ @identity_properties = identity_properties
10
+ @signer_properties = signer_properties
11
+ end
12
+
13
+ # @return [String]
14
+ attr_reader :scheme_id
15
+
16
+ # @return [Hash]
17
+ attr_reader :identity_properties
18
+
19
+ # @return [Hash]
20
+ attr_reader :signer_properties
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smithy
4
+ module Client
5
+ # Base class for all AuthScheme classes.
6
+ class AuthScheme
7
+ def initialize(scheme_id:, signer:, identity_type:)
8
+ @scheme_id = scheme_id
9
+ @signer = signer
10
+ @identity_type = identity_type
11
+ end
12
+
13
+ # @return [String]
14
+ attr_reader :scheme_id
15
+
16
+ # @return [IdentityProvider, nil]
17
+ def identity_provider(identity_provider = {})
18
+ identity_provider[@identity_type]
19
+ end
20
+
21
+ # @return [Signer]
22
+ attr_reader :signer
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smithy
4
+ module Client
5
+ module AuthSchemes
6
+ # Auth scheme for no authentication.
7
+ class Anonymous < AuthScheme
8
+ def initialize(options = {})
9
+ super(
10
+ scheme_id: 'smithy.api#noAuth',
11
+ signer: options.fetch(:signer, Signers::Anonymous.new),
12
+ identity_type: Identities::Anonymous
13
+ )
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smithy
4
+ module Client
5
+ module AuthSchemes
6
+ # Auth scheme for HTTP API keys.
7
+ class HttpApiKey < AuthScheme
8
+ def initialize(options = {})
9
+ super(
10
+ scheme_id: 'smithy.api#httpApiKeyAuth',
11
+ signer: options.fetch(:signer, Signers::HttpApiKey.new),
12
+ identity_type: Identities::HttpApiKey
13
+ )
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smithy
4
+ module Client
5
+ module AuthSchemes
6
+ # Auth scheme for HTTP Basic.
7
+ class HttpBasic < AuthScheme
8
+ def initialize(options = {})
9
+ super(
10
+ scheme_id: 'smithy.api#httpBasicAuth',
11
+ signer: options.fetch(:signer, Signers::HttpBasic.new),
12
+ identity_type: Identities::HttpLogin
13
+ )
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smithy
4
+ module Client
5
+ module AuthSchemes
6
+ # Auth scheme for HTTP Bearer tokens.
7
+ class HttpBearer < AuthScheme
8
+ def initialize(options = {})
9
+ super(
10
+ scheme_id: 'smithy.api#httpBearerAuth',
11
+ signer: options.fetch(:signer, Signers::HttpBearer.new),
12
+ identity_type: Identities::HttpBearer
13
+ )
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smithy
4
+ module Client
5
+ module AuthSchemes
6
+ # Auth scheme for HTTP Digest.
7
+ class HttpDigest < AuthScheme
8
+ def initialize(options = {})
9
+ super(
10
+ scheme_id: 'smithy.api#httpDigestAuth',
11
+ signer: options.fetch(:signer, Signers::HttpDigest.new),
12
+ identity_type: Identities::HttpLogin
13
+ )
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smithy
4
+ module Client
5
+ # Base class for all service clients.
6
+ class Base
7
+ include HandlerBuilder
8
+
9
+ def initialize(plugins, options)
10
+ @config = build_config(plugins, options)
11
+ @handlers = build_handler_list(plugins)
12
+ after_initialize(plugins)
13
+ end
14
+
15
+ # @return [Struct]
16
+ attr_reader :config
17
+
18
+ # @return [HandlerList]
19
+ attr_reader :handlers
20
+
21
+ # Builds and returns a {Request} for the named operation. The request will not have been sent.
22
+ # @param [Symbol] operation_name
23
+ # @return [Request]
24
+ def build_request(operation_name, params = {})
25
+ Request.new(
26
+ handlers: @handlers.for(operation_name),
27
+ context: context_for(operation_name, params)
28
+ )
29
+ end
30
+
31
+ # @return [Array<Symbol>] Returns a list of valid request operation
32
+ # names. These are valid arguments to {#build_input} and are also
33
+ # valid methods.
34
+ def operation_names
35
+ self.class.service.operation_names
36
+ end
37
+
38
+ # @api private
39
+ def inspect
40
+ "#<#{self.class.name || 'Smithy::Client::Base'}>"
41
+ end
42
+
43
+ private
44
+
45
+ # Constructs a {Configuration} object and gives each plugin the
46
+ # opportunity to register options with default values.
47
+ def build_config(plugins, options)
48
+ config = Configuration.new
49
+ config.add_option(:service)
50
+ plugins.each do |plugin|
51
+ plugin.add_options(config) if plugin.respond_to?(:add_options)
52
+ end
53
+ config.build!(options.merge(service: self.class.service))
54
+ end
55
+
56
+ # Gives each plugin the opportunity to register handlers for this client.
57
+ def build_handler_list(plugins)
58
+ plugins.each_with_object(HandlerList.new) do |plugin, handlers|
59
+ plugin.add_handlers(handlers, @config) if plugin.respond_to?(:add_handlers)
60
+ end
61
+ end
62
+
63
+ # Gives each plugin the opportunity to modify this client.
64
+ def after_initialize(plugins)
65
+ plugins.reverse.each do |plugin|
66
+ plugin.after_initialize(self) if plugin.respond_to?(:after_initialize)
67
+ end
68
+ end
69
+
70
+ # @return [HandlerContext]
71
+ def context_for(operation_name, params)
72
+ HandlerContext.new(
73
+ operation_name: operation_name,
74
+ operation: config.service.operation(operation_name),
75
+ client: self,
76
+ params: params,
77
+ config: config
78
+ )
79
+ end
80
+
81
+ def waiter(waiter_name, options = {})
82
+ waiter_class = waiters[waiter_name]
83
+ raise Waiters::NoSuchWaiterError.new(waiter_name, waiters.keys) unless waiter_class
84
+
85
+ waiter_class.new(options.merge(client: self))
86
+ end
87
+
88
+ class << self
89
+ def new(options = {})
90
+ plugins = build_plugins
91
+ options = options.dup
92
+ options[:plugins]&.freeze
93
+ before_initialize(plugins, options)
94
+ client = allocate
95
+ client.send(:initialize, plugins, options)
96
+ client
97
+ end
98
+
99
+ # Registers a plugin with this client.
100
+ #
101
+ # @example Register a plugin
102
+ #
103
+ # ClientClass.add_plugin(PluginClass)
104
+ #
105
+ # @example Register a plugin with an object
106
+ #
107
+ # plugin = MyPluginClass.new(options)
108
+ # ClientClass.add_plugin(plugin)
109
+ #
110
+ # @param [Class, Symbol, String, Object] plugin
111
+ # @see .clear_plugins
112
+ # @see .set_plugins
113
+ # @see .remove_plugin
114
+ # @see .plugins
115
+ # @return [void]
116
+ def add_plugin(plugin)
117
+ @plugins.add(plugin)
118
+ end
119
+
120
+ # @see .clear_plugins
121
+ # @see .set_plugins
122
+ # @see .add_plugin
123
+ # @see .plugins
124
+ # @return [void]
125
+ def remove_plugin(plugin)
126
+ @plugins.remove(plugin)
127
+ end
128
+
129
+ # @see .set_plugins
130
+ # @see .add_plugin
131
+ # @see .remove_plugin
132
+ # @see .plugins
133
+ # @return [void]
134
+ def clear_plugins
135
+ @plugins.set([])
136
+ end
137
+
138
+ # @param [Array<Plugin>] plugins
139
+ # @see .clear_plugins
140
+ # @see .add_plugin
141
+ # @see .remove_plugin
142
+ # @see .plugins
143
+ # @return [void]
144
+ def plugins=(plugins)
145
+ @plugins.set(plugins)
146
+ end
147
+
148
+ # Returns the list of registered plugins for this Client. Plugins are
149
+ # inherited from the client super class when the client is defined.
150
+ # @see .clear_plugins
151
+ # @see .set_plugins
152
+ # @see .add_plugin
153
+ # @see .remove_plugin
154
+ # @return [Array<Plugin>]
155
+ def plugins
156
+ Array(@plugins).freeze
157
+ end
158
+
159
+ # @return [Schema::Shapes::ServiceShape]
160
+ def service
161
+ @service ||= Schema::Shapes::ServiceShape.new
162
+ end
163
+
164
+ # @param [ServiceShape] service
165
+ attr_writer :service
166
+
167
+ # @option options [ServiceShape] :service (ServiceShape.new)
168
+ # @option options [Array<Plugin>] :plugins ([]) A list of plugins to
169
+ # add to the client class created.
170
+ # @return [Class<Client::Base>]
171
+ def define(options = {})
172
+ subclass = Class.new(self)
173
+ subclass.service = options[:service] || service
174
+ Array(options[:plugins]).each do |plugin|
175
+ subclass.add_plugin(plugin)
176
+ end
177
+ subclass
178
+ end
179
+ alias extend define
180
+
181
+ private
182
+
183
+ def build_plugins
184
+ plugins.map { |plugin| plugin.is_a?(Class) ? plugin.new : plugin }
185
+ end
186
+
187
+ def before_initialize(plugins, options)
188
+ plugins.each do |plugin|
189
+ plugin.before_initialize(self, options) if plugin.respond_to?(:before_initialize)
190
+ end
191
+ end
192
+
193
+ def inherited(subclass)
194
+ super
195
+ subclass.instance_variable_set('@plugins', PluginList.new)
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smithy
4
+ module Client
5
+ # IO object for response targets.
6
+ class BlockIO
7
+ # @param [Hash] headers (nil)
8
+ # @param [Proc] block
9
+ def initialize(headers = nil, &block)
10
+ @headers = headers
11
+ @block = block
12
+ @size = 0
13
+ end
14
+
15
+ # @return [Integer]
16
+ attr_reader :size
17
+
18
+ # @param [String] chunk
19
+ # @return [Integer]
20
+ def write(chunk)
21
+ @block.call(chunk, @headers)
22
+ chunk.bytesize
23
+ ensure
24
+ chunk.bytesize.tap { |chunk_size| @size += chunk_size }
25
+ end
26
+
27
+ # @param [Integer] bytes (nil)
28
+ # @param [String] output_buffer (nil)
29
+ # @return [String, nil]
30
+ def read(bytes = nil, output_buffer = nil)
31
+ data = bytes ? nil : ''
32
+ output_buffer ? output_buffer.replace(data || '') : data
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smithy
4
+ module Client
5
+ # Configuration is used to define possible configuration options and
6
+ # then build read-only structures with user-supplied data.
7
+ #
8
+ # ## Adding Configuration Options
9
+ #
10
+ # Add configuration options with optional default values. These are used
11
+ # when building configuration objects.
12
+ #
13
+ # configuration = Configuration.new
14
+ #
15
+ # configuration.add_option(:max_retries, 3)
16
+ # configuration.add_option(:use_ssl, true)
17
+ #
18
+ # cfg = configuration.build!
19
+ # #=> #<struct max_retires=3 use_ssl=true>
20
+ #
21
+ # ## Building Configuration Objects
22
+ #
23
+ # Calling {#build!} on a {Configuration} object causes it to return
24
+ # a read-only (frozen) struct. Options passed to {#build!} are merged
25
+ # on top of any default options.
26
+ #
27
+ # configuration = Configuration.new
28
+ # configuration.add_option(:color, 'red')
29
+ #
30
+ # # default
31
+ # cfg1 = configuration.build!
32
+ # cfg1.color #=> 'red'
33
+ #
34
+ # # supplied color
35
+ # cfg2 = configuration.build!(color: 'blue')
36
+ # cfg2.color #=> 'blue'
37
+ #
38
+ # ## Accepted Options
39
+ #
40
+ # If you try to {#build!} a {Configuration} object with an unknown
41
+ # option, an `ArgumentError` is raised.
42
+ #
43
+ # configuration = Configuration.new
44
+ # configuration.add_option(:color)
45
+ # configuration.add_option(:size)
46
+ # configuration.add_option(:category)
47
+ #
48
+ # configuration.build!(price: 100)
49
+ # #=> raises an ArgumentError, :price was not added as an option
50
+ #
51
+ class Configuration
52
+ # @api private
53
+ Defaults = Class.new(Array) do
54
+ def each(&block)
55
+ reverse.to_a.each(&block)
56
+ end
57
+ end
58
+
59
+ def initialize
60
+ @defaults = Hash.new { |h, k| h[k] = Defaults.new }
61
+ end
62
+
63
+ # Adds a getter method that returns the named option or a default
64
+ # value. Default values can be passed as a static positional argument
65
+ # or via a block.
66
+ #
67
+ # # defaults to nil
68
+ # configuration.add_option(:name)
69
+ #
70
+ # # with a string default
71
+ # configuration.add_option(:name, 'John Doe')
72
+ #
73
+ # # with a dynamic default value, evaluated once when calling #build!
74
+ # configuration.add_option(:name, 'John Doe')
75
+ # configuration.add_option(:username) do |config|
76
+ # config.name.gsub(/\W+/, '').downcase
77
+ # end
78
+ # cfg = configuration.build!
79
+ # cfg.name #=> 'John Doe'
80
+ # cfg.username #=> 'johndoe'
81
+ #
82
+ # @param [Symbol] name The name of the configuration option. This will
83
+ # be used to define a getter by the same name.
84
+ #
85
+ # @param [Object] default The default value for this option. You can specify
86
+ # a default by passing a value, a `Proc` object or a block argument.
87
+ # Procs and blocks are evaluated when {#build!} is called.
88
+ #
89
+ # @return [self]
90
+ def add_option(name, default = nil, &block)
91
+ default = DynamicDefault.new(block) if block_given?
92
+ @defaults[name.to_sym] << default
93
+ self
94
+ end
95
+
96
+ # Constructs and returns a configuration structure.
97
+ # Values not present in `options` will default to those supplied via
98
+ # add option.
99
+ #
100
+ # configuration = Configuration.new
101
+ # configuration.add_option(:enabled, true)
102
+ #
103
+ # cfg1 = configuration.build!
104
+ # cfg1.enabled #=> true
105
+ #
106
+ # cfg2 = configuration.build!(enabled: false)
107
+ # cfg2.enabled #=> false
108
+ #
109
+ # If you pass in options to `#build!` that have not been defined,
110
+ # then an `ArgumentError` will be raised.
111
+ #
112
+ # configuration = Configuration.new
113
+ # configuration.add_option(:enabled, true)
114
+ #
115
+ # # oops, spelling error for :enabled
116
+ # cfg = configuration.build!(enabld: true)
117
+ # #=> raises ArgumentError
118
+ #
119
+ # The object returned is a frozen `Struct`.
120
+ #
121
+ # configuration = Configuration.new
122
+ # configuration.add_option(:enabled, true)
123
+ #
124
+ # cfg = configuration.build!
125
+ # cfg.enabled #=> true
126
+ # cfg[:enabled] #=> true
127
+ # cfg['enabled'] #=> true
128
+ #
129
+ # @param [Hash] options ({}) A hash of configuration options.
130
+ # @return [Struct] Returns a frozen configuration `Struct`.
131
+ def build!(options = {})
132
+ struct = empty_struct
133
+ apply_options(struct, options)
134
+ apply_defaults(struct, options)
135
+ struct
136
+ end
137
+
138
+ private
139
+
140
+ def empty_struct
141
+ Struct.new(*@defaults.keys.sort).new
142
+ end
143
+
144
+ def apply_options(struct, options)
145
+ options.each do |opt, value|
146
+ struct[opt] = value
147
+ rescue NameError
148
+ msg = "invalid configuration option `#{opt.inspect}'"
149
+ raise ArgumentError, msg
150
+ end
151
+ end
152
+
153
+ def apply_defaults(struct, options)
154
+ @defaults.each do |opt_name, defaults|
155
+ struct[opt_name] = defaults unless options.key?(opt_name)
156
+ end
157
+ DefaultResolver.new(struct).resolve
158
+ end
159
+
160
+ # @api private
161
+ class DynamicDefault
162
+ attr_accessor :block
163
+
164
+ def initialize(block = nil)
165
+ @block = block
166
+ end
167
+
168
+ def call(*)
169
+ @block.call(*)
170
+ end
171
+ end
172
+
173
+ # @api private
174
+ class DefaultResolver
175
+ def initialize(struct)
176
+ @struct = struct
177
+ @members = Set.new(@struct.members)
178
+ end
179
+
180
+ def resolve
181
+ @members.each { |opt_name| value_at(opt_name) }
182
+ end
183
+
184
+ def respond_to?(method_name, *args)
185
+ @members.include?(method_name) or super
186
+ end
187
+
188
+ private
189
+
190
+ def value_at(opt_name)
191
+ value = @struct[opt_name]
192
+ if value.is_a?(Defaults)
193
+ resolve_defaults(opt_name, value)
194
+ else
195
+ value
196
+ end
197
+ end
198
+
199
+ def resolve_defaults(opt_name, defaults)
200
+ defaults.each do |default|
201
+ default = default.call(self) if default.is_a?(DynamicDefault)
202
+ @struct[opt_name] = default
203
+ break unless default.nil?
204
+ end
205
+ @struct[opt_name]
206
+ end
207
+
208
+ def respond_to_missing?(method_name, *args)
209
+ @members.include?(method_name) || super
210
+ end
211
+
212
+ def method_missing(method_name, *args)
213
+ if @members.include?(method_name)
214
+ value_at(method_name)
215
+ else
216
+ super
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end