tlaw 0.0.2 → 0.1.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -0
  3. data/CHANGELOG.md +18 -2
  4. data/README.md +10 -7
  5. data/examples/demo_base.rb +2 -2
  6. data/examples/experimental/README.md +3 -0
  7. data/examples/experimental/afterthedeadline.rb +22 -0
  8. data/examples/experimental/airvisual.rb +14 -0
  9. data/examples/experimental/apixu.rb +32 -0
  10. data/examples/experimental/bing_maps.rb +18 -0
  11. data/examples/experimental/currencylayer.rb +25 -0
  12. data/examples/experimental/earthquake.rb +29 -0
  13. data/examples/experimental/freegeoip.rb +16 -0
  14. data/examples/experimental/geonames.rb +98 -0
  15. data/examples/experimental/isfdb.rb +17 -0
  16. data/examples/experimental/musicbrainz.rb +27 -0
  17. data/examples/experimental/nominatim.rb +52 -0
  18. data/examples/experimental/omdb.rb +68 -0
  19. data/examples/experimental/open_exchange_rates.rb +36 -0
  20. data/examples/experimental/open_route.rb +27 -0
  21. data/examples/experimental/open_street_map.rb +16 -0
  22. data/examples/experimental/quandl.rb +50 -0
  23. data/examples/experimental/reddit.rb +25 -0
  24. data/examples/experimental/swapi.rb +27 -0
  25. data/examples/experimental/tmdb.rb +53 -0
  26. data/examples/experimental/world_bank.rb +85 -0
  27. data/examples/experimental/world_bank_climate.rb +77 -0
  28. data/examples/experimental/wunderground.rb +66 -0
  29. data/examples/experimental/wunderground_demo.rb +7 -0
  30. data/examples/forecast_io.rb +16 -16
  31. data/examples/giphy.rb +4 -4
  32. data/examples/giphy_demo.rb +1 -1
  33. data/examples/open_weather_map.rb +64 -60
  34. data/examples/open_weather_map_demo.rb +4 -4
  35. data/examples/tmdb_demo.rb +1 -1
  36. data/examples/urbandictionary_demo.rb +2 -2
  37. data/lib/tlaw.rb +14 -15
  38. data/lib/tlaw/api.rb +108 -26
  39. data/lib/tlaw/api_path.rb +86 -87
  40. data/lib/tlaw/data_table.rb +15 -10
  41. data/lib/tlaw/dsl.rb +126 -224
  42. data/lib/tlaw/dsl/api_builder.rb +47 -0
  43. data/lib/tlaw/dsl/base_builder.rb +108 -0
  44. data/lib/tlaw/dsl/endpoint_builder.rb +26 -0
  45. data/lib/tlaw/dsl/namespace_builder.rb +86 -0
  46. data/lib/tlaw/endpoint.rb +63 -85
  47. data/lib/tlaw/formatting.rb +55 -0
  48. data/lib/tlaw/formatting/describe.rb +86 -0
  49. data/lib/tlaw/formatting/inspect.rb +52 -0
  50. data/lib/tlaw/namespace.rb +141 -98
  51. data/lib/tlaw/param.rb +45 -141
  52. data/lib/tlaw/param/type.rb +36 -49
  53. data/lib/tlaw/response_processors.rb +81 -0
  54. data/lib/tlaw/util.rb +16 -33
  55. data/lib/tlaw/version.rb +6 -3
  56. data/tlaw.gemspec +9 -9
  57. metadata +63 -13
  58. data/lib/tlaw/param_set.rb +0 -111
  59. data/lib/tlaw/response_processor.rb +0 -126
data/lib/tlaw/api_path.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'forwardable'
2
4
 
3
5
  module TLAW
@@ -7,130 +9,127 @@ module TLAW
7
9
  class APIPath
8
10
  class << self
9
11
  # @private
10
- attr_accessor :base_url, :path, :xml, :docs_link
11
-
12
- # @private
13
- def symbol
14
- # FIXME: the second part is necessary only for describes,
15
- # and probably should not be here.
16
- @symbol || (name && "#{name}.new")
17
- end
18
-
12
+ attr_reader :symbol, :parent, :path, :param_defs, :description, :docs_link
19
13
  # @private
20
- CLASS_NAMES = {
21
- :[] => 'Element'
22
- }.freeze
14
+ attr_writer :parent
23
15
 
24
16
  # @private
25
- def class_name
26
- # TODO:
27
- # * validate if it is classifiable
28
- # * provide additional option for non-default class name
29
- CLASS_NAMES[symbol] || Util.camelize(symbol.to_s)
17
+ def define(**args)
18
+ Class.new(self).tap do |subclass|
19
+ subclass.setup(**args)
20
+ end
30
21
  end
31
22
 
32
23
  # @private
33
- def description=(descr)
34
- @description = Util::Description.new(descr)
24
+ def definition
25
+ {
26
+ symbol: symbol,
27
+ path: path,
28
+ description: description,
29
+ docs_link: docs_link,
30
+ params: param_defs&.map { |p| [p.name, p.to_h] }.to_h || {}
31
+ }
35
32
  end
36
33
 
37
34
  # @private
38
- def description
39
- return unless @description || @docs_link
40
-
41
- Util::Description.new(
42
- [@description, ("Docs: #{@docs_link}" if @docs_link)]
43
- .compact.join("\n\n")
44
- )
35
+ def is_defined? # rubocop:disable Naming/PredicateName
36
+ !symbol.nil?
45
37
  end
46
38
 
47
39
  # @private
48
- def inherit(namespace, **attrs)
49
- Class.new(self).tap do |subclass|
50
- attrs.each { |a, v| subclass.send("#{a}=", v) }
51
- namespace.const_set(subclass.class_name, subclass)
52
- end
40
+ def full_param_defs
41
+ [*parent&.full_param_defs, *param_defs]
53
42
  end
54
43
 
55
44
  # @private
56
- def params_from_path!
57
- Addressable::Template.new(path).keys.each do |key|
58
- param_set.add key.to_sym, keyword: false
59
- end
45
+ def required_param_defs
46
+ param_defs.select(&:required?)
60
47
  end
61
48
 
62
49
  # @private
63
- def setup_parents(parent)
64
- param_set.parent = parent.param_set
65
- response_processor.parent = parent.response_processor
50
+ def url_template
51
+ parent&.url_template or fail "Orphan path #{path}, can't determine full URL"
52
+ [parent.url_template, path].join
66
53
  end
67
54
 
68
- # @private
69
- def symbol=(sym)
70
- @symbol = sym
71
- @path ||= "/#{sym}"
55
+ # @return [Array<Class>]
56
+ def parents
57
+ Util.parents(self)
72
58
  end
73
59
 
74
- # @return [ParamSet]
75
- def param_set
76
- @param_set ||= ParamSet.new
77
- end
60
+ protected
78
61
 
79
- # @private
80
- def response_processor
81
- @response_processor ||= ResponseProcessor.new
62
+ def setup(symbol:, path:, param_defs: [], description: nil, docs_link: nil)
63
+ self.symbol = symbol
64
+ self.path = path
65
+ self.param_defs = param_defs
66
+ self.description = description
67
+ self.param_defs = param_defs
68
+ self.docs_link = docs_link
82
69
  end
83
70
 
84
- # @private
85
- def to_method_definition
86
- "#{symbol}(#{param_set.to_code})"
87
- end
71
+ attr_writer :symbol, :param_defs, :path, :description, :xml, :docs_link
72
+ end
88
73
 
89
- # Redefined on descendants, it just allows you to do `api.namespace.describe`
90
- # or `api.namespace1.namespace2.endpoints[:my_endpoint].describe`
91
- # and have reasonable useful description printed.
92
- #
93
- # @return [Util::Description] It is just description string but with
94
- # redefined `#inspect` to be pretty-printed in console.
95
- def describe(definition = nil)
96
- Util::Description.new(
97
- ".#{definition || to_method_definition}" +
98
- (description ? "\n" + description.indent(' ') + "\n" : '') +
99
- (param_set.empty? ? '' : "\n" + param_set.describe.indent(' '))
100
- )
101
- end
74
+ extend Forwardable
102
75
 
103
- # @private
104
- def describe_short
105
- Util::Description.new(
106
- ".#{to_method_definition}" +
107
- (description ? "\n" + description_first_para.indent(' ') : '')
108
- )
109
- end
76
+ # @private
77
+ attr_reader :parent, :params
110
78
 
111
- # @private
112
- def define_method_on(host)
113
- file, line = method(:to_code).source_location
114
- # line + 1 is where real definition, theoretically, starts
115
- host.module_eval(to_code, file, line + 1)
116
- end
79
+ def initialize(parent, **params)
80
+ @parent = parent
81
+ @params = params
82
+ end
83
+
84
+ # @return [Array<APIPath>]
85
+ def parents
86
+ Util.parents(self)
87
+ end
117
88
 
118
- private
89
+ def_delegators :self_class, :describe
119
90
 
120
- def description_first_para
121
- description.split("\n\n").first
122
- end
91
+ # @private
92
+ # Could've been protected, but it hurts testability :shrug:
93
+ def prepared_params
94
+ (parent&.prepared_params || {}).merge(prepare_params(@params))
123
95
  end
124
96
 
125
- extend Forwardable
97
+ protected
126
98
 
127
- def initialize(**parent_params)
128
- @parent_params = parent_params
99
+ def api
100
+ is_a?(API) ? self : parent&.api
129
101
  end
130
102
 
131
103
  private
132
104
 
133
- def object_class
105
+ def_delegators :self_class, :param_defs, :required_param_defs
106
+
107
+ def prepare_params(arguments)
108
+ guard_missing!(arguments)
109
+ guard_unknown!(arguments)
110
+
111
+ param_defs
112
+ .map { |dfn| [dfn, arguments[dfn.name]] }
113
+ .reject { |_, v| v.nil? }
114
+ .map { |dfn, arg| dfn.(arg) }
115
+ .inject(&:merge)
116
+ &.transform_keys(&:to_sym) || {}
117
+ end
118
+
119
+ def guard_unknown!(arguments)
120
+ arguments.keys.-(param_defs.map(&:name)).yield_self { |unknown|
121
+ unknown.empty? or fail ArgumentError, "Unknown arguments: #{unknown.join(', ')}"
122
+ }
123
+ end
124
+
125
+ def guard_missing!(arguments)
126
+ required_param_defs.map(&:name).-(arguments.keys).yield_self { |missing|
127
+ missing.empty? or fail ArgumentError, "Missing arguments: #{missing.join(', ')}"
128
+ }
129
+ end
130
+
131
+ # For def_delegators
132
+ def self_class
134
133
  self.class
135
134
  end
136
135
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module TLAW
2
4
  # Basically, just a 2-d array with column names. Or you can think of
3
5
  # it as an array of hashes. Or loose DataFrame implementation.
@@ -40,15 +42,11 @@ module TLAW
40
42
  #
41
43
  # @param hashes [Array<Hash>]
42
44
  def initialize(hashes)
43
- hashes = hashes.each_with_index.map { |h, i|
44
- h.is_a?(Hash) or
45
- fail ArgumentError,
46
- "All rows are expected to be hashes, row #{i} is #{h.class}"
47
-
48
- h.map { |k, v| [k.to_s, v] }.to_h
49
- }
45
+ hashes = hashes.each_with_index(&method(:enforce_hash!))
46
+ .map { |h| h.transform_keys(&:to_s) }
50
47
  empty = hashes.map(&:keys).flatten.uniq.map { |k| [k, nil] }.to_h
51
- hashes = hashes.map { |h| empty.merge(h) }
48
+ hashes = hashes.map(&empty.method(:merge))
49
+
52
50
  super(hashes)
53
51
  end
54
52
 
@@ -109,8 +107,15 @@ module TLAW
109
107
  end
110
108
 
111
109
  # @private
112
- def pretty_print(pp)
113
- pp.text("#<#{self.class.name}[#{keys.join(', ')}] x #{size}>")
110
+ def pretty_print(printer)
111
+ printer.text("#<#{self.class.name}[#{keys.join(', ')}] x #{size}>")
112
+ end
113
+
114
+ private
115
+
116
+ def enforce_hash!(val, idx)
117
+ val.is_a?(Hash) or fail ArgumentError,
118
+ "All rows are expected to be hashes, row #{idx} is #{val.class}"
114
119
  end
115
120
  end
116
121
  end
data/lib/tlaw/dsl.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module TLAW
2
4
  # This module is core of a TLAW API definition. It works like this:
3
5
  #
@@ -21,17 +23,15 @@ module TLAW
21
23
  #
22
24
  module DSL
23
25
  # @!method base(url)
24
- # Allows to set entire API base URL, all endpoints and namespaces
25
- # pathes are calculated relative to it.
26
+ # Set entire API base URL, all endpoints and namespaces pathes are calculated relative to it.
26
27
  #
27
28
  # **Works for:** API
28
29
  #
29
30
  # @param url [String]
30
31
 
31
32
  # @!method desc(text)
32
- # Allows to set description string for your API object. It can
33
- # be multiline, and TLAW will automatically un-indent excessive
34
- # indentations:
33
+ # Set description string for API object (namespace or endpoint). It can be multiline, and
34
+ # TLAW will automatically un-indent excessive indentations:
35
35
  #
36
36
  # ```ruby
37
37
  # # ...several levels of indents while you create a definition
@@ -41,7 +41,7 @@ module TLAW
41
41
  # }
42
42
  #
43
43
  # # ...but when you are using it...
44
- # p my_api.endpoints[:endpoint].describe
44
+ # p my_api.endpoint(:endpoint).describe
45
45
  # # This is some endpoint.
46
46
  # # And it works!
47
47
  # # ....
@@ -52,8 +52,7 @@ module TLAW
52
52
  # @param text [String]
53
53
 
54
54
  # @!method docs(link)
55
- # Allows to add link to documentation as a separate line to
56
- # object description. Just to be semantic :)
55
+ # Add link to documentation as a separate line to object description. Just to be semantic :)
57
56
  #
58
57
  # ```ruby
59
58
  # # you do something like
@@ -62,7 +61,7 @@ module TLAW
62
61
  # docs "http://docs.example.com/my/endpoint"
63
62
  #
64
63
  # # ...and then somewhere...
65
- # p my_api.endpoints[:endpoint].describe
64
+ # p my_api.endpoint(:endpoint).describe
66
65
  # # That is my endpoint.
67
66
  # #
68
67
  # # Docs: http://docs.example.com/my/endpoint
@@ -78,19 +77,17 @@ module TLAW
78
77
  #
79
78
  # Param defnition defines several things:
80
79
  #
81
- # * how method definition to call this namespace/endpoint would
82
- # look like: whether the parameter is keyword or regular argument,
83
- # whether it is required and what is default value otherwise;
84
- # * how parameter is processed: converted and validated from passed
85
- # value;
86
- # * how param is sent to target API: how it will be called in
87
- # the query string and formatted on call.
80
+ # * how method definition to call this namespace/endpoint would look like: whether the
81
+ # parameter is keyword or regular argument, whether it is required and what is default
82
+ # value otherwise;
83
+ # * how parameter is processed: converted and validated from passed value;
84
+ # * how param is sent to target API: how it will be called in the query string and formatted
85
+ # on call.
88
86
  #
89
87
  # Note also those things about params:
90
88
  #
91
- # * as described in {#namespace} and {#endpoint}, setting path template
92
- # will implicitly set params. You can rewrite this on implicit
93
- # param call, for ex:
89
+ # * as described in {#namespace} and {#endpoint}, setting path template will implicitly set
90
+ # params. You can rewrite this on implicit param call, for ex:
94
91
  #
95
92
  # ```ruby
96
93
  # endpoint :foo, '/foo/{bar}'
@@ -110,8 +107,7 @@ module TLAW
110
107
  # # call-sequence now is foo(bar, baz:)
111
108
  # ```
112
109
  #
113
- # * param of outer namespace are passed to API on call from inner
114
- # namespaces and endpoints, for ex:
110
+ # * param of outer namespace are passed to API on call to inner namespaces and endpoints, for ex:
115
111
  #
116
112
  # ```ruby
117
113
  # namespace :city do
@@ -130,39 +126,37 @@ module TLAW
130
126
  # **Works for:** API, namespace, endpoint
131
127
  #
132
128
  # @param name [Symbol] Parameter name
133
- # @param type [Class, Symbol] Expected parameter type. Could by
134
- # some class (then parameter would be checked for being instance
135
- # of this class or it would be `ArgumentError`), or duck type
136
- # (method name that parameter value should respond to).
137
- # @param keyword [true, false] Whether the param will go as a
138
- # keyword param to method definition.
139
- # @param required [true, false] Whether this param is required.
140
- # It will be considered on method definition.
129
+ # @param type [Class, Symbol] Expected parameter type. Could by some class (then parameter
130
+ # would be checked for being instance of this class or it would be `ArgumentError`), or
131
+ # duck type (method name that parameter value should respond to).
132
+ # @param keyword [true, false] Whether the param will go as a keyword param to method definition.
133
+ # @param required [true, false] Whether this param is required. It will be considered on
134
+ # method definition.
141
135
  # @param opts [Hash] Options
142
- # @option opts [Symbol] :field What the field would be called in
143
- # API query string (it would be `name` by default).
144
- # @option opts [#to_proc] :format How to format this option before
145
- # including into URL. By default, it is just `.to_s`.
146
- # @option opts [String] :desc Param description. You could do it
147
- # multiline and with indents, like {#desc}.
148
- # @option opts :default Default value for this param. Would be
149
- # rendered in method definition and then passed to target API
150
- # _(TODO: in future, there also would be "invisible" params,
151
- # that are just passed to target, always the same, as well as
152
- # params that aren't passed at all if user gave default value.)_
153
- # @option opts [Hash, Array] :enum Whether parameter only accepts
154
- # enumerated values. Two forms are accepted:
136
+ # @option opts [Symbol] :field What the field would be called in API query string (it would be
137
+ # param's name by default).
138
+ # @option opts [#to_proc] :format How to format this option before including into URL. By
139
+ # default, it is just `.to_s`.
140
+ # @option opts [String] :desc Params::Base description. You could do it multiline and with
141
+ # indents, like {#desc}.
142
+ # @option opts :default Default value for this param. Would be rendered in method definition
143
+ # and then passed to target API _(TODO: in future, there also would be "invisible" params,
144
+ # that are just passed to target, always the same, as well as params that aren't passed at
145
+ # all if user gave default value.)_
146
+ # @option opts [Hash, Array] :enum Whether parameter only accepts enumerated values. Two forms
147
+ # are accepted:
155
148
  #
156
149
  # ```ruby
157
- # # array form
150
+ # # Enumerable form
158
151
  # param :units, enum: %i[us metric britain]
159
- # # parameter accepts only :us, :metric, :britain values, and
160
- # # passes them to target API as is
152
+ # # parameter accepts only :us, :metric, :britain values, and passes them to target API as is
153
+ # # any Enumerable is OK:
154
+ # param :count, enum: 1..5
155
+ # # ^ note that means [1, 2, 3, 4, 5], not "any Numeric between 1 and 5"
161
156
  #
162
157
  # # hash "accepted => passed" form
163
158
  # param :compact, enum: {true => 'gzip', false => nil}
164
- # # parameter accepts true or false, on true passes "compact=gzip",
165
- # # on false passes nothing.
159
+ # # parameter accepts true or false, on true passes "compact=gzip", on false passes nothing.
166
160
  # ```
167
161
 
168
162
  # @!method namespace(name, path = nil, &block)
@@ -170,23 +164,20 @@ module TLAW
170
164
  #
171
165
  # {Namespace} has two roles:
172
166
  #
173
- # * on Ruby API, defines how you access to the final endpoint,
174
- # like `api.namespace1.namespace2(some_param).endpoint(...)`
167
+ # * on Ruby API, defines how you access to the final endpoint, like
168
+ # `api.namespace1.namespace2(some_param).endpoint(...)`
175
169
  # * on calling API, it adds its path to entire URL.
176
170
  #
177
- # **NB:** If you call `namespace(:something)` and it was already defined,
178
- # current definition will be added to existing one (but it can't
179
- # change path of existing one, which is reasonable).
171
+ # **NB:** If you call `namespace(:something)` and it was already defined, current definition
172
+ # will be added to existing one (but it can't change path of existing one, which is reasonable).
180
173
  #
181
174
  # **Works for:** API, namespace
182
175
  #
183
- # @param name [Symbol] Name of the method by which namespace would
184
- # be accessible.
176
+ # @param name [Symbol] Name of the method by which namespace would be accessible.
185
177
  # @param path [String] Path to add to API inside this namespace.
186
- # When not provided, considered to be `/<name>`. When provided,
187
- # taken literally (no slashes or other symbols added). Note, that
188
- # you can use `/../` in path, redesigning someone else's APIs on
189
- # the fly. Also, you can use [RFC 6570](https://www.rfc-editor.org/rfc/rfc6570.txt)
178
+ # When not provided, considered to be `/<name>`. When provided, taken literally (no slashes
179
+ # or other symbols added). Note, that you can use `/../` in path, redesigning someone else's
180
+ # APIs on the fly. Also, you can use [RFC 6570](https://www.rfc-editor.org/rfc/rfc6570.txt)
190
181
  # URL templates to mark params going straightly into URI.
191
182
  #
192
183
  # Some examples:
@@ -208,10 +199,9 @@ module TLAW
208
199
  # # method quux(id = nil), API URL http://api.example.com/foo/quux/123
209
200
  # # ...where 123 is what you've passed as id
210
201
  # ```
211
- # @param block Definition of current namespace params, and
212
- # namespaces and endpoints inside current.
213
- # Note that by defining params inside this block, you can change
214
- # namespace's method call sequence.
202
+ # @param block Definition of current namespace params, and namespaces and endpoints inside
203
+ # current. Note that by defining params inside this block, you can change namespace's method
204
+ # call sequence.
215
205
  #
216
206
  # For example:
217
207
  #
@@ -231,42 +221,35 @@ module TLAW
231
221
  # # call-sequence: foo(bar, baz:)
232
222
  # ```
233
223
  #
234
- # ...and so on. See also {#param} for understanding what you
235
- # can change here.
236
- #
224
+ # ...and so on. See also {#param} for understanding what you can change here.
237
225
 
238
226
  # @!method endpoint(name, path = nil, **opts, &block)
239
227
  # Defines new endpoint or updates existing one.
240
228
  #
241
- # {Endpoint} is the thing doing the real work: providing Ruby API
242
- # method to really call target API.
229
+ # {Endpoint} is the thing doing the real work: providing Ruby API method to really call target
230
+ # API.
243
231
  #
244
- # **NB:** If you call `endpoint(:something)` and it was already defined,
245
- # current definition will be added to existing one (but it can't
246
- # change path of existing one, which is reasonable).
232
+ # **NB:** If you call `endpoint(:something)` and it was already defined, current definition
233
+ # will be added to existing one (but it can't change path of existing one, which is reasonable).
247
234
  #
248
235
  # **Works for:** API, namespace
249
236
  #
250
- # @param name [Symbol] Name of the method by which endpoint would
251
- # be accessible.
237
+ # @param name [Symbol] Name of the method by which endpoint would be accessible.
252
238
  # @param path [String] Path to call API from this endpoint.
253
- # When not provided, considered to be `/<name>`. When provided,
254
- # taken literally (no slashes or other symbols added). Note, that
255
- # you can use `/../` in path, redesigning someone else's APIs on
256
- # the fly. Also, you can use [RFC 6570](https://www.rfc-editor.org/rfc/rfc6570.txt)
239
+ # When not provided, considered to be `/<name>`. When provided, taken literally (no slashes
240
+ # or other symbols added). Note, that you can use `/../` in path, redesigning someone else's
241
+ # APIs on the fly. Also, you can use [RFC 6570](https://www.rfc-editor.org/rfc/rfc6570.txt)
257
242
  # URL templates to mark params going straightly into URI.
258
243
  #
259
244
  # Look at {#namespace} for examples, idea is the same.
260
245
  #
261
246
  # @param opts [Hash] Some options, currently only `:xml`.
262
- # @option opts [true, false] :xml Whether endpoint's response should
263
- # be parsed as XML (JSON otherwise & by default). Parsing in this
264
- # case is performed with [crack](https://github.com/jnunemaker/crack),
265
- # producing the hash, to which all other rules of post-processing
266
- # are applied.
267
- # @param block Definition of endpoint's params and docs.
268
- # Note that by defining params inside this block, you can change
269
- # endpoints's method call sequence.
247
+ # @option opts [true, false] :xml Whether endpoint's response should be parsed as XML (JSON
248
+ # otherwise & by default). Parsing in this case is performed with
249
+ # [crack](https://github.com/jnunemaker/crack), producing the hash, to which all other rules
250
+ # of post-processing are applied.
251
+ # @param block Definition of endpoint's params and docs. Note that by defining params inside
252
+ # this block, you can change endpoints's method call sequence.
270
253
  #
271
254
  # For example:
272
255
  #
@@ -286,47 +269,42 @@ module TLAW
286
269
  # # call-sequence: foo(bar, baz:)
287
270
  # ```
288
271
  #
289
- # ...and so on. See also {#param} for understanding what you
290
- # can change here.
272
+ # ...and so on. See also {#param} for understanding what you can change here.
291
273
 
292
274
  # @!method post_process(key = nil, &block)
293
275
  # Sets post-processors for response.
294
276
  #
295
- # There are also {#post_process_replace} (for replacing entire
296
- # response with something else) and {#post_process_items} (for
297
- # post-processing each item of sub-array).
277
+ # There are also {#post_process_replace} (for replacing entire response with something else)
278
+ # and {#post_process_items} (for post-processing each item of sub-array).
298
279
  #
299
280
  # Notes:
300
281
  #
301
- # * you can set any number of post-processors of any kind, and they
302
- # will be applied in exactly the same order they are set;
303
- # * you can set post-processors in parent namespace (or for entire
304
- # API), in this case post-processors of _outer_ namespace are
305
- # always applied before inner ones. That allow you to define some
306
- # generic parsing/rewriting on API level, then more specific
307
- # key postprocessors on endpoints;
308
- # * hashes are flattened again after _each_ post-processor, so if
309
- # for some `key` you'll return `{count: 1, continue: false}`,
310
- # response hash will immediately have
311
- # `{"key.count" => 1, "key.continue" => false}`.
282
+ # * You can set any number of post-processors of any kind, and they will be applied in exactly
283
+ # the same order they are set.
284
+ # * You can set post-processors in parent namespace (or for entire API), in this case
285
+ # post-processors of _outer_ namespace are always applied before inner ones. That allow you
286
+ # to define some generic parsing/rewriting on API level, then more specific key
287
+ # postprocessors on endpoints. But only post-processors defined BEFORE the nested object
288
+ # definition would be taken into account.
289
+ # * Hashes are flattened again after _each_ post-processor, so if for some `key` you'll
290
+ # return `{count: 1, continue: false}`, response hash will immediately have
291
+ # `{"key.count" => 1, "key.continue" => false}`. TODO: Probably it is subject to change.
312
292
  #
313
293
  # @overload post_process(&block)
314
- # Sets post-processor for whole response. Note, that in this case
315
- # _return_ value of block is ignored, it is expected that your
316
- # block will receive response and modify it inplace, like this:
294
+ # Sets post-processor for whole response. Note, that in this case _return_ value of block
295
+ # is ignored, it is expected that your block will receive response and modify it inplace,
296
+ # like this:
317
297
  #
318
298
  # ```ruby
319
299
  # post_process do |response|
320
300
  # response['coord'] = Geo::Coord.new(response['lat'], response['lng'])
321
301
  # end
322
302
  # ```
323
- # If you need to replace entire response with something else,
324
- # see {#post_process_replace}
303
+ # If you need to replace entire response with something else, see {#post_process_replace}
325
304
  #
326
305
  # @overload post_process(key, &block)
327
- # Sets post-processor for one response key. Post-processor is
328
- # called only if key exists in the response, and value by this
329
- # key is replaced with post-processor's response.
306
+ # Sets post-processor for one response key. Post-processor is called only if key exists in
307
+ # the response, and value by this key is replaced with post-processor's response.
330
308
  #
331
309
  # Note, that if `block` returns `nil`, key will be removed completely.
332
310
  #
@@ -341,13 +319,11 @@ module TLAW
341
319
  # @param key [String]
342
320
 
343
321
  # @!method post_process_items(key, &block)
344
- # Sets post-processors for each items of array, being at `key` (if
345
- # the key is present in response, and if its value is array of
346
- # hashes).
322
+ # Sets post-processors for each items of array, being at `key` (if the key is present in
323
+ # response, and if its value is array of hashes).
347
324
  #
348
- # Inside `block` you can use {#post_process} method as described
349
- # above (but all of its actions will be related only to current
350
- # item of array).
325
+ # Inside `block` you can use {#post_process} method as described above (but all of its actions
326
+ # will be related only to current item of array).
351
327
  #
352
328
  # Example:
353
329
  #
@@ -377,11 +353,9 @@ module TLAW
377
353
  # @param key [String]
378
354
 
379
355
  # @!method post_process_replace(&block)
380
- # Just like {#post_process} for entire response, but _replaces_
381
- # it with what block returns.
356
+ # Just like {#post_process} for entire response, but _replaces_ it with what block returns.
382
357
  #
383
- # Real-life usage: WorldBank API typically returns responses this
384
- # way:
358
+ # Real-life usage: WorldBank API typically returns responses this way:
385
359
  #
386
360
  # ```json
387
361
  # [
@@ -389,8 +363,8 @@ module TLAW
389
363
  # {"some_data_variable": [{}, {}, {}]}
390
364
  # ]
391
365
  # ```
392
- # ...e.g. metadata and real response as two items in array, not
393
- # two keys in hash. We can easily fix this:
366
+ # ...e.g. metadata and real response as two items in array, not two keys in hash. We can
367
+ # easily fix this:
394
368
  #
395
369
  # ```ruby
396
370
  # post_process_replace do |response|
@@ -400,112 +374,40 @@ module TLAW
400
374
  #
401
375
  # See also {#post_process} for some generic explanation of post-processing.
402
376
 
403
- # @private
404
- class BaseWrapper
405
- def initialize(object)
406
- @object = object
407
- end
408
-
409
- def define(&block)
410
- instance_eval(&block)
411
- end
412
-
413
- def description(text)
414
- # first, remove spaces at a beginning of each line
415
- # then, remove empty lines before and after docs block
416
- @object.description =
417
- text
418
- .gsub(/^[ \t]+/, '')
419
- .gsub(/\A\n|\n\s*\Z/, '')
420
- end
421
-
422
- alias_method :desc, :description
423
-
424
- def docs(link)
425
- @object.docs_link = link
426
- end
427
-
428
- def param(name, type = nil, **opts)
429
- @object.param_set.add(name, **opts.merge(type: type))
430
- end
431
-
432
- def post_process(key = nil, &block)
433
- @object.response_processor.add_post_processor(key, &block)
434
- end
435
-
436
- def post_process_replace(&block)
437
- @object.response_processor.add_replacer(&block)
438
- end
439
-
440
- class PostProcessProxy
441
- def initialize(parent_key, parent)
442
- @parent_key = parent_key
443
- @parent = parent
444
- end
445
-
446
- def post_process(key = nil, &block)
447
- @parent.add_item_post_processor(@parent_key, key, &block)
448
- end
449
- end
450
-
451
- def post_process_items(key, &block)
452
- PostProcessProxy
453
- .new(key, @object.response_processor)
454
- .instance_eval(&block)
455
- end
456
- end
457
-
458
- # @private
459
- class EndpointWrapper < BaseWrapper
460
- end
461
-
462
- # @private
463
- class NamespaceWrapper < BaseWrapper
464
- def endpoint(name, path = nil, **opts, &block)
465
- update_existing(Endpoint, name, path, **opts, &block) ||
466
- add_child(Endpoint, name, path: path || "/#{name}", **opts, &block)
467
- end
468
-
469
- def namespace(name, path = nil, &block)
470
- update_existing(Namespace, name, path, &block) ||
471
- add_child(Namespace, name, path: path || "/#{name}", &block)
472
- end
473
-
474
- private
475
-
476
- WRAPPERS = {
477
- Endpoint => EndpointWrapper,
478
- Namespace => NamespaceWrapper
479
- }.freeze
480
-
481
- def update_existing(child_class, name, path, **opts, &block)
482
- existing = @object.children[name] or return nil
483
- existing < child_class or
484
- fail ArgumentError, "#{name} is already defined as #{child_class == Endpoint ? 'namespace' : 'endpoint'}, you can't redefine it as #{child_class}"
485
-
486
- !path && opts.empty? or
487
- fail ArgumentError, "#{child_class} is already defined, you can't change its path or options"
488
-
489
- WRAPPERS[child_class].new(existing).define(&block) if block
490
- end
377
+ # @!method shared_def(name, &block)
378
+ # Define reusable parts of definition, which can be utilized with {#use_def}. Common example
379
+ # is pagination (for one API, pagination logic for all paginated endpoints is typically the
380
+ # same):
381
+ #
382
+ # ```ruby
383
+ # # at "whole API definition" level:
384
+ # shared_def :pagination do
385
+ # param :page, Integer, field: :pg, desc: "Page number, starts from 0"
386
+ # param :per_page, enum: (5..50), field: :per, desc: "Number of items per page"
387
+ # end
388
+ #
389
+ # # at each particulare endpoint definition, just
390
+ # use_def :pagination
391
+ # # ...instead of repeating the param description above.
392
+ # ```
393
+ #
394
+ # Shared definition block may contain any DSL elements.
395
+ #
396
+ # **Works for:** API, namespace, endpoint
491
397
 
492
- def add_child(child_class, name, **opts, &block)
493
- @object.add_child(
494
- child_class.inherit(@object, symbol: name, **opts)
495
- .tap { |c| c.setup_parents(@object) }
496
- .tap(&:params_from_path!)
497
- .tap { |c|
498
- WRAPPERS[child_class].new(c).define(&block) if block
499
- }
500
- )
501
- end
502
- end
398
+ # @!method use_def(name)
399
+ # Use shared definition defined earlier. See {#shared_def} for explanation and examples.
400
+ #
401
+ # **Works for:** API, namespace, endpoint
503
402
 
504
403
  # @private
505
- class APIWrapper < NamespaceWrapper
506
- def base(url)
507
- @object.base_url = url
508
- end
509
- end
404
+ # If there is no content in class, YARD got mad with directives.
405
+ # See: https://github.com/lsegal/yard/issues/1207
406
+ DUMMY = nil
510
407
  end
511
408
  end
409
+
410
+ require_relative 'dsl/api_builder'
411
+ require_relative 'dsl/base_builder'
412
+ require_relative 'dsl/endpoint_builder'
413
+ require_relative 'dsl/namespace_builder'