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
@@ -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
|