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
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TLAW
|
4
|
+
# Just a bit of formatting utils.
|
5
|
+
module Formatting
|
6
|
+
# Description is just a String subclass with rewritten `inspect`
|
7
|
+
# implementation (useful in `irb`/`pry`):
|
8
|
+
#
|
9
|
+
# ```ruby
|
10
|
+
# str = "Description of endpoint:\nIt has params:..."
|
11
|
+
# # "Description of endpoint:\nIt has params:..."
|
12
|
+
#
|
13
|
+
# TLAW::Formatting::Description.new(str)
|
14
|
+
# # Description of endpoint:
|
15
|
+
# # It has params:...
|
16
|
+
# ```
|
17
|
+
#
|
18
|
+
# TLAW uses it when responds to {Namespace.describe}/{Endpoint.describe}.
|
19
|
+
#
|
20
|
+
class Description < String
|
21
|
+
alias inspect to_s
|
22
|
+
|
23
|
+
def initialize(str)
|
24
|
+
super(str.to_s.gsub(/ +\n/, "\n"))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
module_function
|
29
|
+
|
30
|
+
# @private
|
31
|
+
def call_sequence(klass)
|
32
|
+
params = params_to_ruby(klass.param_defs)
|
33
|
+
name = klass < API ? "#{klass.name}.new" : klass.symbol.to_s
|
34
|
+
params.empty? ? name : "#{name}(#{params})"
|
35
|
+
end
|
36
|
+
|
37
|
+
# @private
|
38
|
+
def params_to_ruby(params) # rubocop:disable Metrics/AbcSize
|
39
|
+
key, arg = params.partition(&:keyword?)
|
40
|
+
req_arg, opt_arg = arg.partition(&:required?)
|
41
|
+
req_key, opt_key = key.partition(&:required?)
|
42
|
+
|
43
|
+
# FIXME: default.inspect will fail with, say, Time
|
44
|
+
[
|
45
|
+
*req_arg.map { |p| p.name.to_s },
|
46
|
+
*opt_arg.map { |p| "#{p.name}=#{p.default.inspect}" },
|
47
|
+
*req_key.map { |p| "#{p.name}:" },
|
48
|
+
*opt_key.map { |p| "#{p.name}: #{p.default.inspect}" }
|
49
|
+
].join(', ')
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
require_relative 'formatting/inspect'
|
55
|
+
require_relative 'formatting/describe'
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TLAW
|
4
|
+
module Formatting
|
5
|
+
# @private
|
6
|
+
module Describe
|
7
|
+
class << self
|
8
|
+
def endpoint_class(klass)
|
9
|
+
[
|
10
|
+
Formatting.call_sequence(klass),
|
11
|
+
klass.description&.yield_self { |desc| "\n" + indent(desc, ' ') },
|
12
|
+
klass.docs_link&.yield_self(&"\n Docs: %s".method(:%)),
|
13
|
+
param_defs(klass.param_defs)
|
14
|
+
].compact.join("\n").yield_self(&Description.method(:new))
|
15
|
+
end
|
16
|
+
|
17
|
+
def namespace_class(klass)
|
18
|
+
[
|
19
|
+
endpoint_class(klass),
|
20
|
+
nested(klass.namespaces, 'Namespaces'),
|
21
|
+
nested(klass.endpoints, 'Endpoints')
|
22
|
+
].join.yield_self(&Description.method(:new))
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def short(klass)
|
28
|
+
descr = klass.description&.yield_self { |d| "\n" + indent(first_para(d), ' ') }
|
29
|
+
".#{Formatting.call_sequence(klass)}#{descr}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def nested(klasses, title)
|
33
|
+
return '' if klasses.empty?
|
34
|
+
|
35
|
+
"\n\n #{title}:\n\n" +
|
36
|
+
klasses.map(&method(:short))
|
37
|
+
.map { |cd| indent(cd, ' ') }
|
38
|
+
.join("\n\n")
|
39
|
+
end
|
40
|
+
|
41
|
+
def param_defs(defs)
|
42
|
+
return nil if defs.empty?
|
43
|
+
|
44
|
+
defs
|
45
|
+
.map(&method(:param_def))
|
46
|
+
.join("\n")
|
47
|
+
.yield_self { |s| "\n" + indent(s, ' ') }
|
48
|
+
end
|
49
|
+
|
50
|
+
def param_def(param)
|
51
|
+
[
|
52
|
+
'@param',
|
53
|
+
param.name,
|
54
|
+
doc_type(param.type)&.yield_self { |t| "[#{t}]" },
|
55
|
+
param.description,
|
56
|
+
possible_values(param.type),
|
57
|
+
param.default&.yield_self { |d| "(default = #{d.inspect})" }
|
58
|
+
].compact.join(' ').gsub(/ +\n/, "\n")
|
59
|
+
end
|
60
|
+
|
61
|
+
def possible_values(type)
|
62
|
+
return unless type.respond_to?(:possible_values)
|
63
|
+
|
64
|
+
"\n Possible values: #{type.possible_values}"
|
65
|
+
end
|
66
|
+
|
67
|
+
def doc_type(type)
|
68
|
+
case type
|
69
|
+
when Param::ClassType
|
70
|
+
type.type.name
|
71
|
+
when Param::DuckType
|
72
|
+
"##{type.type}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def first_para(str)
|
77
|
+
str.split("\n\n").first
|
78
|
+
end
|
79
|
+
|
80
|
+
def indent(str, indentation = ' ')
|
81
|
+
str.gsub(/(\A|\n)/, '\1' + indentation)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TLAW
|
4
|
+
module Formatting
|
5
|
+
# @private
|
6
|
+
module Inspect
|
7
|
+
class << self
|
8
|
+
def endpoint(object)
|
9
|
+
_object(object)
|
10
|
+
end
|
11
|
+
|
12
|
+
def namespace(object)
|
13
|
+
_object(object, children_list(object.class))
|
14
|
+
end
|
15
|
+
|
16
|
+
def endpoint_class(klass)
|
17
|
+
_class(klass, 'endpoint')
|
18
|
+
end
|
19
|
+
|
20
|
+
def namespace_class(klass)
|
21
|
+
_class(klass, 'namespace', children_list(klass))
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def children_list(namespace)
|
27
|
+
ns = " namespaces: #{namespace.namespaces.map(&:symbol).join(', ')};" \
|
28
|
+
unless namespace.namespaces.empty?
|
29
|
+
ep = " endpoints: #{namespace.endpoints.map(&:symbol).join(', ')};" \
|
30
|
+
unless namespace.endpoints.empty?
|
31
|
+
|
32
|
+
[ns, ep].compact.join
|
33
|
+
end
|
34
|
+
|
35
|
+
def _object(object, addition = '')
|
36
|
+
"#<#{object.class.name}(" +
|
37
|
+
object.params.map { |name, val| "#{name}: #{val.inspect}" }.join(', ') +
|
38
|
+
');' +
|
39
|
+
addition +
|
40
|
+
' docs: .describe>'
|
41
|
+
end
|
42
|
+
|
43
|
+
def _class(klass, type, addition = '')
|
44
|
+
(klass.name || "(unnamed #{type} class)") +
|
45
|
+
"(call-sequence: #{Formatting.call_sequence(klass)};" +
|
46
|
+
addition +
|
47
|
+
' docs: .describe)'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/lib/tlaw/namespace.rb
CHANGED
@@ -1,159 +1,202 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module TLAW
|
2
|
-
# Namespace is
|
3
|
-
# nest Ruby calls (like `api.namespace1.namespace2.real_call(params)`),
|
4
|
-
# optionally providing some parameters while nesting, like
|
5
|
-
# `worldbank.countries('uk').population(2016)`.
|
6
|
-
#
|
7
|
-
# By default, namespaces nesting also means URL nesting (e.g.
|
8
|
-
# `base_url/namespace1/namespace2/endpoint`), but that could be altered
|
9
|
-
# on namespace definition, see {DSL} module for details.
|
4
|
+
# Namespace is a grouping tool for API endpoints.
|
10
5
|
#
|
11
|
-
#
|
12
|
-
# or subclasses by yourself: you use {DSL} for their definition and
|
13
|
-
# then call `.<namespace_name>` method on parent namespace (or API instance):
|
6
|
+
# Assuming we have this API definition:
|
14
7
|
#
|
15
8
|
# ```ruby
|
16
|
-
# class
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
9
|
+
# class OpenWeatherMap < TLAW::API
|
10
|
+
# define do
|
11
|
+
# base 'http://api.openweathermap.org/data/2.5'
|
12
|
+
#
|
13
|
+
# namespace :current, '/weather' do
|
14
|
+
# endpoint :city, '?q={city}{,country_code}'
|
15
|
+
# end
|
20
16
|
# end
|
21
17
|
# end
|
18
|
+
# ```
|
19
|
+
#
|
20
|
+
# We can now use it this way:
|
22
21
|
#
|
23
|
-
#
|
24
|
-
# api =
|
22
|
+
# ```ruby
|
23
|
+
# api = OpenWeatherMap.new
|
24
|
+
# api.namespaces
|
25
|
+
# # => [OpenWeatherMap::Current(call-sequence: current; endpoints: city; docs: .describe)]
|
26
|
+
# api.current
|
27
|
+
# # => #<OpenWeatherMap::Current(); endpoints: city; docs: .describe>
|
28
|
+
# # OpenWeatherMap::Current is dynamically generated class, descendant from Namespace,
|
29
|
+
# # it is inspectable and usable for future calls
|
25
30
|
#
|
26
|
-
# api.
|
27
|
-
#
|
28
|
-
#
|
31
|
+
# api.current.describe
|
32
|
+
# # current
|
33
|
+
# #
|
34
|
+
# # Endpoints:
|
35
|
+
# #
|
36
|
+
# # .city(city=nil, country_code=nil)
|
37
|
+
#
|
38
|
+
# api.current.city('Kharkiv', 'UA')
|
39
|
+
# # => real API call at /weather?q=Kharkiv,UA
|
40
|
+
# ```
|
41
|
+
#
|
42
|
+
# Namespaces are useful for logical endpoint grouping and allow providing additional params to
|
43
|
+
# them. When params are defined for namespace by DSL, the call could look like this:
|
44
|
+
#
|
45
|
+
# ```ruby
|
46
|
+
# worldbank.countries('uk').population(2016)
|
47
|
+
# # ^^^^^^^^^^^^^^ ^
|
48
|
+
# # namespace :countries have |
|
49
|
+
# # defined country_code parameter |
|
50
|
+
# # all namespace and endpoint params would be passed to endpoint call,
|
51
|
+
# # so real API call would probably look like ...?country=uk&year=2016
|
29
52
|
# ```
|
30
53
|
#
|
54
|
+
# See {DSL} for more details on namespaces, endpoints and params definitions.
|
55
|
+
#
|
31
56
|
class Namespace < APIPath
|
32
57
|
class << self
|
33
58
|
# @private
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
59
|
+
TRAVERSE_RESTRICTION = {
|
60
|
+
endpoints: Endpoint,
|
61
|
+
namespaces: Namespace,
|
62
|
+
nil => APIPath
|
63
|
+
}.freeze
|
64
|
+
|
65
|
+
# Traverses through all of the children (depth-first). Yields them into a block specified,
|
66
|
+
# or returns `Enumerator` if no block was passed.
|
67
|
+
#
|
68
|
+
# @yield [Namespace or Endpoint]
|
69
|
+
# @param restrict_to [Symbol] `:endpoints` or `:namespaces` to traverse only children of
|
70
|
+
# specified class; if not passed, traverses all of them.
|
71
|
+
# @return [Enumerator, self] Enumerator is returned if no block passed.
|
72
|
+
def traverse(restrict_to = nil, &block)
|
73
|
+
return to_enum(:traverse, restrict_to) unless block_given?
|
74
|
+
|
75
|
+
klass = TRAVERSE_RESTRICTION.fetch(restrict_to)
|
76
|
+
children.each do |child|
|
77
|
+
yield child if child < klass
|
78
|
+
child.traverse(restrict_to, &block) if child.respond_to?(:traverse)
|
39
79
|
end
|
80
|
+
self
|
81
|
+
end
|
82
|
+
|
83
|
+
# Returns the namespace's child of the requested name.
|
84
|
+
#
|
85
|
+
# @param name [Symbol]
|
86
|
+
# @param restrict_to [Class] `Namespace` or `Endpoint`
|
87
|
+
# @return [Array<APIPath>]
|
88
|
+
def child(name, restrict_to: APIPath)
|
89
|
+
child_index[name]
|
90
|
+
.tap { |child| validate_class(name, child, restrict_to) }
|
40
91
|
end
|
41
92
|
|
42
|
-
# Lists all current namespace's nested namespaces
|
93
|
+
# Lists all current namespace's nested namespaces.
|
43
94
|
#
|
44
|
-
# @return [
|
95
|
+
# @return [Array<Namespace>]
|
45
96
|
def namespaces
|
46
|
-
children.
|
97
|
+
children.grep(Namespace.singleton_class)
|
47
98
|
end
|
48
99
|
|
49
|
-
#
|
100
|
+
# Returns the namespace's nested namespaces of the requested name.
|
50
101
|
#
|
51
|
-
# @
|
102
|
+
# @param name [Symbol]
|
103
|
+
# @return [Namespace]
|
104
|
+
def namespace(name)
|
105
|
+
child(name, restrict_to: Namespace)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Lists all current namespace's endpoints.
|
109
|
+
#
|
110
|
+
# @return [Array<Endpoint>]
|
52
111
|
def endpoints
|
53
|
-
children.
|
112
|
+
children.grep(Endpoint.singleton_class)
|
54
113
|
end
|
55
114
|
|
56
|
-
#
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
115
|
+
# Returns the namespace's endpoint of the requested name.
|
116
|
+
#
|
117
|
+
# @param name [Symbol]
|
118
|
+
# @return [Endpoint]
|
119
|
+
def endpoint(name)
|
120
|
+
child(name, restrict_to: Endpoint)
|
61
121
|
end
|
62
122
|
|
123
|
+
# @return [String]
|
63
124
|
def inspect
|
64
|
-
|
65
|
-
"call-sequence: #{symbol}(#{param_set.to_code});" +
|
66
|
-
inspect_docs
|
67
|
-
end
|
125
|
+
return super unless is_defined? || self < API
|
68
126
|
|
69
|
-
|
70
|
-
def inspect_docs
|
71
|
-
inspect_namespaces + inspect_endpoints + ' docs: .describe>'
|
127
|
+
Formatting::Inspect.namespace_class(self)
|
72
128
|
end
|
73
129
|
|
74
|
-
#
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
130
|
+
# Detailed namespace documentation.
|
131
|
+
#
|
132
|
+
# @return [Formatting::Description]
|
133
|
+
def describe
|
134
|
+
return '' unless is_defined?
|
79
135
|
|
80
|
-
|
136
|
+
Formatting::Describe.namespace_class(self)
|
81
137
|
end
|
82
138
|
|
83
139
|
# @private
|
84
|
-
def
|
85
|
-
|
140
|
+
def definition
|
141
|
+
super.merge(children: children)
|
86
142
|
end
|
87
143
|
|
88
|
-
#
|
89
|
-
|
90
|
-
|
91
|
-
#
|
92
|
-
# @return [Util::Description]
|
93
|
-
def describe(definition = nil)
|
94
|
-
super + describe_children
|
144
|
+
# @private
|
145
|
+
def children
|
146
|
+
child_index.values
|
95
147
|
end
|
96
148
|
|
97
|
-
private
|
98
|
-
|
99
|
-
|
100
|
-
return '' if namespaces.empty?
|
101
|
-
" namespaces: #{namespaces.keys.join(', ')};"
|
149
|
+
# @private
|
150
|
+
def child_index
|
151
|
+
@child_index ||= {}
|
102
152
|
end
|
103
153
|
|
104
|
-
|
105
|
-
return '' if endpoints.empty?
|
106
|
-
" endpoints: #{endpoints.keys.join(', ')};"
|
107
|
-
end
|
154
|
+
protected
|
108
155
|
|
109
|
-
def
|
110
|
-
|
156
|
+
def setup(children: [], **args)
|
157
|
+
super(**args)
|
158
|
+
self.children = children.dup.each { |c| c.parent = self }
|
111
159
|
end
|
112
160
|
|
113
|
-
def
|
114
|
-
|
115
|
-
|
116
|
-
|
161
|
+
def children=(children)
|
162
|
+
children.each do |child|
|
163
|
+
child_index[child.symbol] = child
|
164
|
+
end
|
117
165
|
end
|
118
166
|
|
119
|
-
|
120
|
-
return '' if endpoints.empty?
|
167
|
+
private
|
121
168
|
|
122
|
-
|
123
|
-
|
169
|
+
def validate_class(sym, child_class, expected_class)
|
170
|
+
return if child_class&.<(expected_class)
|
124
171
|
|
125
|
-
|
126
|
-
|
127
|
-
|
172
|
+
kind = expected_class.name.split('::').last.downcase.sub('apipath', 'path')
|
173
|
+
fail ArgumentError,
|
174
|
+
"Unregistered #{kind}: #{sym}"
|
128
175
|
end
|
129
176
|
end
|
130
177
|
|
131
|
-
def_delegators :
|
132
|
-
:
|
133
|
-
:
|
134
|
-
:param_set, :describe_short
|
178
|
+
def_delegators :self_class, :symbol,
|
179
|
+
:namespaces, :endpoints,
|
180
|
+
:namespace, :endpoint
|
135
181
|
|
182
|
+
# @return [String]
|
136
183
|
def inspect
|
137
|
-
|
138
|
-
self.class.inspect_docs
|
184
|
+
Formatting::Inspect.namespace(self)
|
139
185
|
end
|
140
186
|
|
141
|
-
|
142
|
-
|
143
|
-
|
187
|
+
# Returns `curl` string to call specified endpoit with specified params from command line.
|
188
|
+
#
|
189
|
+
# @param endpoint [Symbol] Endpoint's name
|
190
|
+
# @param params [Hash] Endpoint's argument
|
191
|
+
# @return [String]
|
192
|
+
def curl(endpoint, **params)
|
193
|
+
child(endpoint, Endpoint, **params).to_curl
|
144
194
|
end
|
145
195
|
|
146
196
|
private
|
147
197
|
|
148
|
-
def child(
|
149
|
-
|
150
|
-
.tap { |child_class|
|
151
|
-
child_class && child_class < expected_class or
|
152
|
-
fail ArgumentError,
|
153
|
-
"Unregistered #{expected_class.name.downcase}: #{symbol}"
|
154
|
-
}.derp { |child_class|
|
155
|
-
child_class.new(@parent_params.merge(params))
|
156
|
-
}
|
198
|
+
def child(sym, expected_class, **params)
|
199
|
+
self.class.child(sym, restrict_to: expected_class).new(self, **params)
|
157
200
|
end
|
158
201
|
end
|
159
202
|
end
|