tlaw 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,67 @@
1
+ require 'open-uri'
2
+ require 'json'
3
+ require 'addressable'
4
+
5
+ # Let no one know! But they in Ruby committee just too long to add
6
+ # something like this to the language.
7
+ #
8
+ # See also https://bugs.ruby-lang.org/issues/12760
9
+ #
10
+ # @private
11
+ class Object
12
+ def derp
13
+ yield self
14
+ end
15
+ end
16
+
17
+ # TLAW is a framework for creating API wrappers for get-only APIs (like
18
+ # weather, geonames and so on) or subsets of APIs (like getting data from
19
+ # Twitter).
20
+ #
21
+ # Short example:
22
+ #
23
+ # ```ruby
24
+ # # Definition:
25
+ # class OpenWeatherMap < TLAW::API
26
+ # param :appid, required: true
27
+ #
28
+ # namespace :current, '/weather' do
29
+ # endpoint :city, '?q={city}{,country_code}' do
30
+ # param :city, required: true
31
+ # end
32
+ # end
33
+ # end
34
+ #
35
+ # # Usage:
36
+ # api = OpenWeatherMap.new(appid: '<yourappid>')
37
+ # api.current.weather('Kharkiv')
38
+ # # => {"weather.main"=>"Clear",
39
+ # # "weather.description"=>"clear sky",
40
+ # # "main.temp"=>8,
41
+ # # "main.pressure"=>1016,
42
+ # # "main.humidity"=>81,
43
+ # # "dt"=>2016-09-19 08:30:00 +0300,
44
+ # # ...}
45
+ #
46
+ # ```
47
+ #
48
+ # Refer to [README](./file/README.md) for reasoning about why you need it and links to
49
+ # more detailed demos, or start reading YARD docs from {API} and {DSL}
50
+ # modules.
51
+ module TLAW
52
+ end
53
+
54
+ require_relative 'tlaw/util'
55
+ require_relative 'tlaw/data_table'
56
+
57
+ require_relative 'tlaw/param'
58
+ require_relative 'tlaw/param_set'
59
+
60
+ require_relative 'tlaw/api_path'
61
+ require_relative 'tlaw/endpoint'
62
+ require_relative 'tlaw/namespace'
63
+ require_relative 'tlaw/api'
64
+
65
+ require_relative 'tlaw/response_processor'
66
+
67
+ require_relative 'tlaw/dsl'
@@ -0,0 +1,58 @@
1
+ module TLAW
2
+ # API is just a top-level {Namespace}.
3
+ #
4
+ # Basically, you start creating your endpoint by descending from API
5
+ # and defining namespaces and endpoints through a {DSL} like this:
6
+ #
7
+ # ```ruby
8
+ # class MyCoolAPI < TLAW::API
9
+ # define do
10
+ # base 'http://api.mycool.com'
11
+ #
12
+ # namespace :awesome do
13
+ # # ...and so on
14
+ # end
15
+ # end
16
+ # end
17
+ # ```
18
+ #
19
+ # And then, you use it:
20
+ #
21
+ # ```ruby
22
+ # api = MyCoolAPI.new
23
+ # api.awesome.cool(param: 'value')
24
+ # ```
25
+ #
26
+ # See {DSL} for explanation of API definition, {Namespace} for explanation
27
+ # of possible usages and {Endpoint} for real calls performing.
28
+ #
29
+ class API < Namespace
30
+ # Thrown when there are an error during call. Contains real URL which
31
+ # was called at the time of an error.
32
+ class Error < RuntimeError
33
+ end
34
+
35
+ class << self
36
+ # Runs the {DSL} inside your API wrapper class.
37
+ def define(&block)
38
+ DSL::APIWrapper.new(self).define(&block)
39
+ end
40
+
41
+ # Returns detailed description of an API, like this:
42
+ #
43
+ # ```ruby
44
+ # MyCoolAPI.describe
45
+ # # MyCoolAPI.new()
46
+ # # This is cool API.
47
+ # #
48
+ # # Namespaces:
49
+ # # .awesome()
50
+ # # This is awesome.
51
+ # ```
52
+ #
53
+ def describe(*)
54
+ super.sub(/\A./, '')
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,137 @@
1
+ require 'forwardable'
2
+
3
+ module TLAW
4
+ # Base class for all API pathes: entire API, namespaces and endpoints.
5
+ # Allows to define params and post-processors on any level.
6
+ #
7
+ class APIPath
8
+ class << self
9
+ # @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
+
19
+ # @private
20
+ CLASS_NAMES = {
21
+ :[] => 'Element'
22
+ }.freeze
23
+
24
+ # @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)
30
+ end
31
+
32
+ # @private
33
+ def description=(descr)
34
+ @description = Util::Description.new(descr)
35
+ end
36
+
37
+ # @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
+ )
45
+ end
46
+
47
+ # @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
53
+ end
54
+
55
+ # @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
60
+ end
61
+
62
+ # @private
63
+ def setup_parents(parent)
64
+ param_set.parent = parent.param_set
65
+ response_processor.parent = parent.response_processor
66
+ end
67
+
68
+ # @private
69
+ def symbol=(sym)
70
+ @symbol = sym
71
+ @path ||= "/#{sym}"
72
+ end
73
+
74
+ # @return [ParamSet]
75
+ def param_set
76
+ @param_set ||= ParamSet.new
77
+ end
78
+
79
+ # @private
80
+ def response_processor
81
+ @response_processor ||= ResponseProcessor.new
82
+ end
83
+
84
+ # @private
85
+ def to_method_definition
86
+ "#{symbol}(#{param_set.to_code})"
87
+ end
88
+
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
102
+
103
+ # @private
104
+ def describe_short
105
+ Util::Description.new(
106
+ ".#{to_method_definition}" +
107
+ (description ? "\n" + description_first_para.indent(' ') : '')
108
+ )
109
+ end
110
+
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
117
+
118
+ private
119
+
120
+ def description_first_para
121
+ description.split("\n\n").first
122
+ end
123
+ end
124
+
125
+ extend Forwardable
126
+
127
+ def initialize(**parent_params)
128
+ @parent_params = parent_params
129
+ end
130
+
131
+ private
132
+
133
+ def object_class
134
+ self.class
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,116 @@
1
+ module TLAW
2
+ # Basically, just a 2-d array with column names. Or you can think of
3
+ # it as an array of hashes. Or loose DataFrame implementation.
4
+ #
5
+ # Just like this:
6
+ #
7
+ # ```ruby
8
+ # tbl = DataTable.new([
9
+ # {id: 1, name: 'Mike', salary: 1000},
10
+ # {id: 2, name: 'Doris', salary: 900},
11
+ # {id: 3, name: 'Angie', salary: 1200}
12
+ # ])
13
+ # # => #<TLAW::DataTable[id, name, salary] x 3>
14
+ # tbl.count
15
+ # # => 3
16
+ # tbl.keys
17
+ # # => ["id", "name", "salary"]
18
+ # tbl[0]
19
+ # # => {"id"=>1, "name"=>"Mike", "salary"=>1000}
20
+ # tbl['salary']
21
+ # # => [1000, 900, 1200]
22
+ # ```
23
+ #
24
+ # Basically, that's it. Every array of hashes in TLAW response will be
25
+ # converted into corresponding `DataTable`.
26
+ #
27
+ class DataTable < Array
28
+ def self.from_columns(column_names, columns)
29
+ from_rows(column_names, columns.transpose)
30
+ end
31
+
32
+ def self.from_rows(column_names, rows)
33
+ new rows.map { |r| column_names.zip(r).to_h }
34
+ end
35
+
36
+ # Creates DataTable from array of hashes.
37
+ #
38
+ # Note, that all hash keys are stringified, and all hashes are expanded
39
+ # to have same set of keys.
40
+ #
41
+ # @param hashes [Array<Hash>]
42
+ 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
+ }
50
+ empty = hashes.map(&:keys).flatten.uniq.map { |k| [k, nil] }.to_h
51
+ hashes = hashes.map { |h| empty.merge(h) }
52
+ super(hashes)
53
+ end
54
+
55
+ # All column names.
56
+ #
57
+ # @return [Array<String>]
58
+ def keys
59
+ empty? ? [] : first.keys
60
+ end
61
+
62
+ # Allows access to one column or row.
63
+ #
64
+ # @overload [](index)
65
+ # Returns one row from a DataTable.
66
+ #
67
+ # @param index [Integer] Row number
68
+ # @return [Hash] Row as a hash
69
+ #
70
+ # @overload [](column_name)
71
+ # Returns one column from a DataTable.
72
+ #
73
+ # @param column_name [String] Name of column
74
+ # @return [Array] Column as an array of all values in it
75
+ #
76
+ def [](index_or_column)
77
+ case index_or_column
78
+ when Integer
79
+ super
80
+ when String, Symbol
81
+ map { |h| h[index_or_column.to_s] }
82
+ else
83
+ fail ArgumentError,
84
+ 'Expected integer or string/symbol index' \
85
+ ", got #{index_or_column.class}"
86
+ end
87
+ end
88
+
89
+ # Slice of a DataTable with only specified columns left.
90
+ #
91
+ # @param names [Array<String>] What columns to leave in a DataTable
92
+ # @return [DataTable]
93
+ def columns(*names)
94
+ names.map!(&:to_s)
95
+ DataTable.new(map { |h| names.map { |n| [n, h[n]] }.to_h })
96
+ end
97
+
98
+ # Represents DataTable as a `column name => all values in columns`
99
+ # hash.
100
+ #
101
+ # @return [Hash{String => Array}]
102
+ def to_h
103
+ keys.map { |k| [k, map { |h| h[k] }] }.to_h
104
+ end
105
+
106
+ # @private
107
+ def inspect
108
+ "#<#{self.class.name}[#{keys.join(', ')}] x #{size}>"
109
+ end
110
+
111
+ # @private
112
+ def pretty_print(pp)
113
+ pp.text("#<#{self.class.name}[#{keys.join(', ')}] x #{size}>")
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,511 @@
1
+ module TLAW
2
+ # This module is core of a TLAW API definition. It works like this:
3
+ #
4
+ # ```ruby
5
+ # class MyAPI < TLAW::API
6
+ # define do # here starts what DSL does
7
+ # namespace :ns do
8
+ #
9
+ # endpoint :es do
10
+ # param :param1, Integer, default: 1
11
+ # end
12
+ # end
13
+ # end
14
+ # end
15
+ # ```
16
+ #
17
+ # Methods of current namespace documentation describe everything you
18
+ # can use inside `define` blocks. Actual structure of things is a bit
19
+ # more complicated (relate to lib/tlaw/dsl.rb if you wish), but current
20
+ # documentation structure considered to be most informative.
21
+ #
22
+ module DSL
23
+ # @!method base(url)
24
+ # Allows to set entire API base URL, all endpoints and namespaces
25
+ # pathes are calculated relative to it.
26
+ #
27
+ # **Works for:** API
28
+ #
29
+ # @param url [String]
30
+
31
+ # @!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:
35
+ #
36
+ # ```ruby
37
+ # # ...several levels of indents while you create a definition
38
+ # desc %Q{
39
+ # This is some endpoint.
40
+ # And it works!
41
+ # }
42
+ #
43
+ # # ...but when you are using it...
44
+ # p my_api.endpoints[:endpoint].describe
45
+ # # This is some endpoint.
46
+ # # And it works!
47
+ # # ....
48
+ # ```
49
+ #
50
+ # **Works for:** API, namespace, endpoint
51
+ #
52
+ # @param text [String]
53
+
54
+ # @!method docs(link)
55
+ # Allows to add link to documentation as a separate line to
56
+ # object description. Just to be semantic :)
57
+ #
58
+ # ```ruby
59
+ # # you do something like
60
+ # desc "That's my endpoint"
61
+ #
62
+ # docs "http://docs.example.com/my/endpoint"
63
+ #
64
+ # # ...and then somewhere...
65
+ # p my_api.endpoints[:endpoint].describe
66
+ # # That is my endpoint.
67
+ # #
68
+ # # Docs: http://docs.example.com/my/endpoint
69
+ # # ....
70
+ # ```
71
+ #
72
+ # **Works for:** API, namespace, endpoint
73
+ #
74
+ # @param link [String]
75
+
76
+ # @!method param(name, type = nil, keyword: true, required: false, **opts)
77
+ # Defines parameter for current API (global), namespace or endpoint.
78
+ #
79
+ # Param defnition defines several things:
80
+ #
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.
88
+ #
89
+ # Note also those things about params:
90
+ #
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:
94
+ #
95
+ # ```ruby
96
+ # endpoint :foo, '/foo/{bar}'
97
+ # # call-sequence would be foo(bar = nil)
98
+ #
99
+ # # But you can make it back keyword:
100
+ # endpoint :foo, '/foo/{bar}' do
101
+ # param :bar, keyword: true, default: 'test'
102
+ # end
103
+ # # call-sequence now is foo(bar: 'test')
104
+ #
105
+ # # Or make it strictly required
106
+ # endpoint :foo, '/foo/{bar}/{baz}' do
107
+ # param :bar, required: true
108
+ # param :baz, keyword: true, required: true
109
+ # end
110
+ # # call-sequence now is foo(bar, baz:)
111
+ # ```
112
+ #
113
+ # * param of outer namespace are passed to API on call from inner
114
+ # namespaces and endpoints, for ex:
115
+ #
116
+ # ```ruby
117
+ # namespace :city do
118
+ # param :city_name
119
+ #
120
+ # namespace :population do
121
+ # endpoint :by_year, '/year/{year}'
122
+ # end
123
+ # end
124
+ #
125
+ # # real call:
126
+ # api.city('London').population.by_year(2015)
127
+ # # Will get http://api.example.com/city/year/2015?city_name=London
128
+ # ```
129
+ #
130
+ # **Works for:** API, namespace, endpoint
131
+ #
132
+ # @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.
141
+ # @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:
155
+ #
156
+ # ```ruby
157
+ # # array form
158
+ # param :units, enum: %i[us metric britain]
159
+ # # parameter accepts only :us, :metric, :britain values, and
160
+ # # passes them to target API as is
161
+ #
162
+ # # hash "accepted => passed" form
163
+ # param :compact, enum: {true => 'gzip', false => nil}
164
+ # # parameter accepts true or false, on true passes "compact=gzip",
165
+ # # on false passes nothing.
166
+ # ```
167
+
168
+ # @!method namespace(name, path = nil, &block)
169
+ # Defines new namespace or updates existing one.
170
+ #
171
+ # {Namespace} has two roles:
172
+ #
173
+ # * on Ruby API, defines how you access to the final endpoint,
174
+ # like `api.namespace1.namespace2(some_param).endpoint(...)`
175
+ # * on calling API, it adds its path to entire URL.
176
+ #
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).
180
+ #
181
+ # **Works for:** API, namespace
182
+ #
183
+ # @param name [Symbol] Name of the method by which namespace would
184
+ # be accessible.
185
+ # @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)
190
+ # URL templates to mark params going straightly into URI.
191
+ #
192
+ # Some examples:
193
+ #
194
+ # ```ruby
195
+ # # assuming API base url is http://api.example.com
196
+ #
197
+ # namespace :foo
198
+ # # method would be foo(), API URL would be http://api.example.com/foo
199
+ #
200
+ # namespace :bar, '/foo/bar'
201
+ # # metod would be bar(), API URL http://api.example.com/foo/bar
202
+ #
203
+ # namespace :baz, ''
204
+ # # method baz(), API URL same as base: useful for gathering into
205
+ # # quazi-namespace from several unrelated endpoints.
206
+ #
207
+ # namespace :quux, '/foo/quux/{id}'
208
+ # # method quux(id = nil), API URL http://api.example.com/foo/quux/123
209
+ # # ...where 123 is what you've passed as id
210
+ # ```
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.
215
+ #
216
+ # For example:
217
+ #
218
+ # ```ruby
219
+ # namespace :foo
220
+ # # call-sequence: foo()
221
+ #
222
+ # namespace :foo do
223
+ # param :bar
224
+ # end
225
+ # # call-sequence: foo(bar: nil)
226
+ #
227
+ # namespace :foo do
228
+ # param :bar, required: true, keyword: false
229
+ # param :baz, required: true
230
+ # end
231
+ # # call-sequence: foo(bar, baz:)
232
+ # ```
233
+ #
234
+ # ...and so on. See also {#param} for understanding what you
235
+ # can change here.
236
+ #
237
+
238
+ # @!method endpoint(name, path = nil, **opts, &block)
239
+ # Defines new endpoint or updates existing one.
240
+ #
241
+ # {Endpoint} is the thing doing the real work: providing Ruby API
242
+ # method to really call target API.
243
+ #
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).
247
+ #
248
+ # **Works for:** API, namespace
249
+ #
250
+ # @param name [Symbol] Name of the method by which endpoint would
251
+ # be accessible.
252
+ # @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)
257
+ # URL templates to mark params going straightly into URI.
258
+ #
259
+ # Look at {#namespace} for examples, idea is the same.
260
+ #
261
+ # @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.
270
+ #
271
+ # For example:
272
+ #
273
+ # ```ruby
274
+ # endpoint :foo
275
+ # # call-sequence: foo()
276
+ #
277
+ # endpoint :foo do
278
+ # param :bar
279
+ # end
280
+ # # call-sequence: foo(bar: nil)
281
+ #
282
+ # endpoint :foo do
283
+ # param :bar, required: true, keyword: false
284
+ # param :baz, required: true
285
+ # end
286
+ # # call-sequence: foo(bar, baz:)
287
+ # ```
288
+ #
289
+ # ...and so on. See also {#param} for understanding what you
290
+ # can change here.
291
+
292
+ # @!method post_process(key = nil, &block)
293
+ # Sets post-processors for response.
294
+ #
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).
298
+ #
299
+ # Notes:
300
+ #
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}`.
312
+ #
313
+ # @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:
317
+ #
318
+ # ```ruby
319
+ # post_process do |response|
320
+ # response['coord'] = Geo::Coord.new(response['lat'], response['lng'])
321
+ # end
322
+ # ```
323
+ # If you need to replace entire response with something else,
324
+ # see {#post_process_replace}
325
+ #
326
+ # @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.
330
+ #
331
+ # Note, that if `block` returns `nil`, key will be removed completely.
332
+ #
333
+ # Usage:
334
+ #
335
+ # ```ruby
336
+ # post_process('date') { |val| Date.parse(val) }
337
+ # # or, btw, just
338
+ # post_process('date', &Date.method(:parse))
339
+ # ```
340
+ #
341
+ # @param key [String]
342
+
343
+ # @!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).
347
+ #
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).
351
+ #
352
+ # Example:
353
+ #
354
+ # Considering API response like:
355
+ #
356
+ # ```json
357
+ # {
358
+ # "meta": {"count": 100},
359
+ # "data": [
360
+ # {"timestamp": "2016-05-01", "value": "10", "dummy": "foo"},
361
+ # {"timestamp": "2016-05-02", "value": "13", "dummy": "bar"}
362
+ # ]
363
+ # }
364
+ # ```
365
+ # ...you can define postprocessing like this:
366
+ #
367
+ # ```ruby
368
+ # post_process_items 'data' do
369
+ # post_process 'timestamp', &Date.method(:parse)
370
+ # post_process 'value', &:to_i
371
+ # post_process('dummy'){nil} # will be removed
372
+ # end
373
+ # ```
374
+ #
375
+ # See also {#post_process} for some generic explanation of post-processing.
376
+ #
377
+ # @param key [String]
378
+
379
+ # @!method post_process_replace(&block)
380
+ # Just like {#post_process} for entire response, but _replaces_
381
+ # it with what block returns.
382
+ #
383
+ # Real-life usage: WorldBank API typically returns responses this
384
+ # way:
385
+ #
386
+ # ```json
387
+ # [
388
+ # {"count": 100, "page": 1},
389
+ # {"some_data_variable": [{}, {}, {}]}
390
+ # ]
391
+ # ```
392
+ # ...e.g. metadata and real response as two items in array, not
393
+ # two keys in hash. We can easily fix this:
394
+ #
395
+ # ```ruby
396
+ # post_process_replace do |response|
397
+ # {meta: response.first, data: response.last}
398
+ # end
399
+ # ```
400
+ #
401
+ # See also {#post_process} for some generic explanation of post-processing.
402
+
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
491
+
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
503
+
504
+ # @private
505
+ class APIWrapper < NamespaceWrapper
506
+ def base(url)
507
+ @object.base_url = url
508
+ end
509
+ end
510
+ end
511
+ end