tlaw 0.0.2 → 0.1.0.pre

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