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