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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -0
- data/CHANGELOG.md +18 -2
- data/README.md +10 -7
- data/examples/demo_base.rb +2 -2
- data/examples/experimental/README.md +3 -0
- data/examples/experimental/afterthedeadline.rb +22 -0
- data/examples/experimental/airvisual.rb +14 -0
- data/examples/experimental/apixu.rb +32 -0
- data/examples/experimental/bing_maps.rb +18 -0
- data/examples/experimental/currencylayer.rb +25 -0
- data/examples/experimental/earthquake.rb +29 -0
- data/examples/experimental/freegeoip.rb +16 -0
- data/examples/experimental/geonames.rb +98 -0
- data/examples/experimental/isfdb.rb +17 -0
- data/examples/experimental/musicbrainz.rb +27 -0
- data/examples/experimental/nominatim.rb +52 -0
- data/examples/experimental/omdb.rb +68 -0
- data/examples/experimental/open_exchange_rates.rb +36 -0
- data/examples/experimental/open_route.rb +27 -0
- data/examples/experimental/open_street_map.rb +16 -0
- data/examples/experimental/quandl.rb +50 -0
- data/examples/experimental/reddit.rb +25 -0
- data/examples/experimental/swapi.rb +27 -0
- data/examples/experimental/tmdb.rb +53 -0
- data/examples/experimental/world_bank.rb +85 -0
- data/examples/experimental/world_bank_climate.rb +77 -0
- data/examples/experimental/wunderground.rb +66 -0
- data/examples/experimental/wunderground_demo.rb +7 -0
- data/examples/forecast_io.rb +16 -16
- data/examples/giphy.rb +4 -4
- data/examples/giphy_demo.rb +1 -1
- data/examples/open_weather_map.rb +64 -60
- data/examples/open_weather_map_demo.rb +4 -4
- data/examples/tmdb_demo.rb +1 -1
- data/examples/urbandictionary_demo.rb +2 -2
- data/lib/tlaw.rb +14 -15
- data/lib/tlaw/api.rb +108 -26
- data/lib/tlaw/api_path.rb +86 -87
- data/lib/tlaw/data_table.rb +15 -10
- data/lib/tlaw/dsl.rb +126 -224
- data/lib/tlaw/dsl/api_builder.rb +47 -0
- data/lib/tlaw/dsl/base_builder.rb +108 -0
- data/lib/tlaw/dsl/endpoint_builder.rb +26 -0
- data/lib/tlaw/dsl/namespace_builder.rb +86 -0
- data/lib/tlaw/endpoint.rb +63 -85
- data/lib/tlaw/formatting.rb +55 -0
- data/lib/tlaw/formatting/describe.rb +86 -0
- data/lib/tlaw/formatting/inspect.rb +52 -0
- data/lib/tlaw/namespace.rb +141 -98
- data/lib/tlaw/param.rb +45 -141
- data/lib/tlaw/param/type.rb +36 -49
- data/lib/tlaw/response_processors.rb +81 -0
- data/lib/tlaw/util.rb +16 -33
- data/lib/tlaw/version.rb +6 -3
- data/tlaw.gemspec +9 -9
- metadata +63 -13
- data/lib/tlaw/param_set.rb +0 -111
- 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
|
-
|
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
|
-
|
21
|
-
:[] => 'Element'
|
22
|
-
}.freeze
|
14
|
+
attr_writer :parent
|
23
15
|
|
24
16
|
# @private
|
25
|
-
def
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
34
|
-
|
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
|
39
|
-
|
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
|
49
|
-
|
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
|
57
|
-
|
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
|
64
|
-
|
65
|
-
|
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
|
-
# @
|
69
|
-
def
|
70
|
-
|
71
|
-
@path ||= "/#{sym}"
|
55
|
+
# @return [Array<Class>]
|
56
|
+
def parents
|
57
|
+
Util.parents(self)
|
72
58
|
end
|
73
59
|
|
74
|
-
|
75
|
-
def param_set
|
76
|
-
@param_set ||= ParamSet.new
|
77
|
-
end
|
60
|
+
protected
|
78
61
|
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
85
|
-
|
86
|
-
"#{symbol}(#{param_set.to_code})"
|
87
|
-
end
|
71
|
+
attr_writer :symbol, :param_defs, :path, :description, :xml, :docs_link
|
72
|
+
end
|
88
73
|
|
89
|
-
|
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
|
-
|
104
|
-
|
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
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
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
|
-
|
89
|
+
def_delegators :self_class, :describe
|
119
90
|
|
120
|
-
|
121
|
-
|
122
|
-
|
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
|
-
|
97
|
+
protected
|
126
98
|
|
127
|
-
def
|
128
|
-
|
99
|
+
def api
|
100
|
+
is_a?(API) ? self : parent&.api
|
129
101
|
end
|
130
102
|
|
131
103
|
private
|
132
104
|
|
133
|
-
|
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
|
data/lib/tlaw/data_table.rb
CHANGED
@@ -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
|
44
|
-
|
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
|
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(
|
113
|
-
|
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
|
-
#
|
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
|
-
#
|
33
|
-
#
|
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.
|
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
|
-
#
|
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.
|
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
|
-
#
|
83
|
-
#
|
84
|
-
# * how parameter is processed: converted and validated from passed
|
85
|
-
#
|
86
|
-
#
|
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
|
-
#
|
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
|
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
|
-
#
|
135
|
-
#
|
136
|
-
#
|
137
|
-
# @param
|
138
|
-
#
|
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
|
-
#
|
144
|
-
# @option opts [#to_proc] :format How to format this option before
|
145
|
-
#
|
146
|
-
# @option opts [String] :desc
|
147
|
-
#
|
148
|
-
# @option opts :default Default value for this param. Would be
|
149
|
-
#
|
150
|
-
#
|
151
|
-
#
|
152
|
-
#
|
153
|
-
#
|
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
|
-
# #
|
150
|
+
# # Enumerable form
|
158
151
|
# param :units, enum: %i[us metric britain]
|
159
|
-
# # parameter accepts only :us, :metric, :britain values, and
|
160
|
-
# #
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
188
|
-
#
|
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
|
-
#
|
213
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
255
|
-
#
|
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
|
-
#
|
264
|
-
#
|
265
|
-
#
|
266
|
-
#
|
267
|
-
#
|
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
|
-
#
|
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
|
-
# *
|
302
|
-
#
|
303
|
-
# *
|
304
|
-
#
|
305
|
-
#
|
306
|
-
#
|
307
|
-
#
|
308
|
-
# *
|
309
|
-
#
|
310
|
-
#
|
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
|
-
#
|
316
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
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
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
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
|
506
|
-
|
507
|
-
|
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'
|