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,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_builder'
|
4
|
+
require_relative 'endpoint_builder'
|
5
|
+
require_relative 'namespace_builder'
|
6
|
+
|
7
|
+
module TLAW
|
8
|
+
module DSL
|
9
|
+
# @private
|
10
|
+
class ApiBuilder < NamespaceBuilder
|
11
|
+
# @private
|
12
|
+
CLASS_NAMES = {
|
13
|
+
:[] => 'Element'
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
def initialize(api_class, &block)
|
17
|
+
@api_class = api_class
|
18
|
+
@definition = {}
|
19
|
+
# super(symbol: nil, children: api_class.children || [], &block)
|
20
|
+
super(symbol: nil, **api_class.definition, &block)
|
21
|
+
end
|
22
|
+
|
23
|
+
def finalize
|
24
|
+
@api_class.setup(**definition)
|
25
|
+
define_children_methods(@api_class)
|
26
|
+
constantize_children(@api_class)
|
27
|
+
end
|
28
|
+
|
29
|
+
def base(url)
|
30
|
+
@definition[:base_url] = url
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def constantize_children(namespace)
|
36
|
+
return unless namespace.name&.match?(/^[A-Z]/) && namespace.respond_to?(:children)
|
37
|
+
|
38
|
+
namespace.children.each do |child|
|
39
|
+
class_name = CLASS_NAMES.fetch(child.symbol, Util.camelize(child.symbol.to_s))
|
40
|
+
# namespace.send(:remove_const, class_name) if namespace.const_defined?(class_name)
|
41
|
+
namespace.const_set(class_name, child) unless namespace.const_defined?(class_name)
|
42
|
+
constantize_children(child)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TLAW
|
4
|
+
module DSL
|
5
|
+
# @private
|
6
|
+
class BaseBuilder
|
7
|
+
attr_reader :params, :processors, :shared_definitions
|
8
|
+
|
9
|
+
def initialize(symbol:, path: nil, context: nil, xml: false, params: {}, **opts, &block)
|
10
|
+
path ||= "/#{symbol}" # Not default arg, because we need to process explicitly passed path: nil, too
|
11
|
+
@definition = opts.merge(symbol: symbol, path: path)
|
12
|
+
@params = params.merge(params_from_path(path))
|
13
|
+
@processors = (context&.processors || []).dup
|
14
|
+
@parser = parser(xml)
|
15
|
+
@shared_definitions = context&.shared_definitions || {}
|
16
|
+
instance_eval(&block) if block
|
17
|
+
end
|
18
|
+
|
19
|
+
def definition
|
20
|
+
@definition.merge(param_defs: params.map { |name, **opts| Param.new(name: name, **opts) })
|
21
|
+
end
|
22
|
+
|
23
|
+
def docs(link)
|
24
|
+
@definition[:docs_link] = link
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def description(text)
|
29
|
+
@definition[:description] = Util.deindent(text)
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
alias desc description
|
34
|
+
|
35
|
+
def param(name, type = nil, enum: nil, desc: nil, description: desc, **opts)
|
36
|
+
opts = opts.merge(
|
37
|
+
type: type || enum&.yield_self(&method(:enum_type)),
|
38
|
+
description: description&.yield_self(&Util.method(:deindent))
|
39
|
+
).compact
|
40
|
+
params.merge!(name => opts) { |_, o, n| o.merge(n) }
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
44
|
+
def shared_def(name, &block)
|
45
|
+
@shared_definitions[name] = block
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
def use_def(name)
|
50
|
+
shared_definitions
|
51
|
+
.fetch(name) { fail ArgumentError, "#{name.inspect} is not a shared definition" }
|
52
|
+
.tap { |block| instance_eval(&block) }
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
def finalize
|
57
|
+
fail NotImplementedError, "#{self.class} doesn't implement #finalize"
|
58
|
+
end
|
59
|
+
|
60
|
+
G = ResponseProcessors::Generators
|
61
|
+
|
62
|
+
def post_process(key_pattern = nil, &block)
|
63
|
+
@processors << (key_pattern ? G.transform_by_key(key_pattern, &block) : G.mutate(&block))
|
64
|
+
end
|
65
|
+
|
66
|
+
# @private
|
67
|
+
class PostProcessProxy
|
68
|
+
def initialize(owner, parent_key)
|
69
|
+
@owner = owner
|
70
|
+
@parent_key = parent_key
|
71
|
+
end
|
72
|
+
|
73
|
+
def post_process(key = nil, &block)
|
74
|
+
@owner.processors << G.transform_nested(@parent_key, key, &block)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def post_process_items(key_pattern, &block)
|
79
|
+
PostProcessProxy.new(self, key_pattern).instance_eval(&block)
|
80
|
+
end
|
81
|
+
|
82
|
+
def post_process_replace(&block)
|
83
|
+
@processors << block
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def parser(xml)
|
89
|
+
xml ? Crack::XML.method(:parse) : JSON.method(:parse)
|
90
|
+
end
|
91
|
+
|
92
|
+
def enum_type(enum)
|
93
|
+
case enum
|
94
|
+
when Hash
|
95
|
+
enum
|
96
|
+
when Enumerable # well... in fact respond_to?(:each) probably will do
|
97
|
+
enum.map { |v| [v, v] }.to_h
|
98
|
+
else
|
99
|
+
fail ArgumentError, "Can't construct enum from #{enum.inspect}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def params_from_path(path)
|
104
|
+
Addressable::Template.new(path).keys.map { |key| [key.to_sym, keyword: false] }.to_h
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_builder'
|
4
|
+
|
5
|
+
module TLAW
|
6
|
+
module DSL
|
7
|
+
# @private
|
8
|
+
class EndpointBuilder < BaseBuilder
|
9
|
+
def definition
|
10
|
+
# TODO: Here we'll be more flexible in future, allowing to avoid flatten/datablize
|
11
|
+
all_processors = [
|
12
|
+
@parser,
|
13
|
+
ResponseProcessors.method(:flatten),
|
14
|
+
*processors,
|
15
|
+
ResponseProcessors.method(:datablize)
|
16
|
+
]
|
17
|
+
|
18
|
+
super.merge(processors: all_processors)
|
19
|
+
end
|
20
|
+
|
21
|
+
def finalize
|
22
|
+
Endpoint.define(**definition)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_builder'
|
4
|
+
require_relative 'endpoint_builder'
|
5
|
+
|
6
|
+
module TLAW
|
7
|
+
module DSL
|
8
|
+
# @private
|
9
|
+
class NamespaceBuilder < BaseBuilder
|
10
|
+
CHILD_CLASSES = {NamespaceBuilder => Namespace, EndpointBuilder => Endpoint}.freeze
|
11
|
+
|
12
|
+
attr_reader :children
|
13
|
+
|
14
|
+
ENDPOINT_METHOD = <<~CODE
|
15
|
+
def %{call_sequence}
|
16
|
+
child(:%{symbol}, Endpoint, %{params}).call
|
17
|
+
end
|
18
|
+
CODE
|
19
|
+
|
20
|
+
NAMESPACE_METHOD = <<~CODE
|
21
|
+
def %{call_sequence}
|
22
|
+
child(:%{symbol}, Namespace, %{params})
|
23
|
+
end
|
24
|
+
CODE
|
25
|
+
|
26
|
+
METHODS = {Endpoint => ENDPOINT_METHOD, Namespace => NAMESPACE_METHOD}.freeze
|
27
|
+
|
28
|
+
def initialize(children: [], **args, &block)
|
29
|
+
@children = children.map { |c| [c.symbol, c] }.to_h
|
30
|
+
super(**args, &block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def definition
|
34
|
+
super.merge(children: children.values)
|
35
|
+
end
|
36
|
+
|
37
|
+
def endpoint(symbol, path = nil, **opts, &block)
|
38
|
+
child(EndpointBuilder, symbol, path, **opts, &block)
|
39
|
+
end
|
40
|
+
|
41
|
+
def namespace(symbol, path = nil, **opts, &block)
|
42
|
+
child(NamespaceBuilder, symbol, path, **opts, &block)
|
43
|
+
end
|
44
|
+
|
45
|
+
def finalize
|
46
|
+
Namespace.define(**definition).tap(&method(:define_children_methods))
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def child(builder_class, symbol, path, **opts, &block)
|
52
|
+
symbol = symbol.to_sym
|
53
|
+
target_class = CHILD_CLASSES.fetch(builder_class)
|
54
|
+
existing = children[symbol]
|
55
|
+
&.tap { |c|
|
56
|
+
c < target_class or fail ArgumentError, "#{symbol} already defined as #{c.ansestors.first}"
|
57
|
+
}
|
58
|
+
&.definition || {}
|
59
|
+
|
60
|
+
builder_class.new(
|
61
|
+
symbol: symbol,
|
62
|
+
path: path,
|
63
|
+
context: self,
|
64
|
+
**opts,
|
65
|
+
**existing,
|
66
|
+
&block
|
67
|
+
).finalize.tap { |child| children[symbol] = child }
|
68
|
+
end
|
69
|
+
|
70
|
+
def define_children_methods(namespace)
|
71
|
+
children.each_value do |child|
|
72
|
+
namespace.module_eval(child_method_code(child))
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def child_method_code(child)
|
77
|
+
params = child.param_defs.map { |par| "#{par.name}: #{par.name}" }.join(', ')
|
78
|
+
METHODS.fetch(child.ancestors[1]) % {
|
79
|
+
call_sequence: Formatting.call_sequence(child),
|
80
|
+
symbol: child.symbol,
|
81
|
+
params: params
|
82
|
+
}
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/tlaw/endpoint.rb
CHANGED
@@ -1,137 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'faraday'
|
2
4
|
require 'faraday_middleware'
|
3
5
|
require 'addressable/template'
|
4
6
|
require 'crack'
|
5
7
|
|
6
8
|
module TLAW
|
7
|
-
#
|
8
|
-
# and processing responses.
|
9
|
-
#
|
10
|
-
# Each real API endpoint is this class descendant, defining its own
|
11
|
-
# params and response processors. On each call small instance of this
|
12
|
-
# class is created, {#call}-ed and dies as you don't need it anymore.
|
9
|
+
# Represents API endpoint.
|
13
10
|
#
|
14
|
-
#
|
15
|
-
# instances directly:
|
11
|
+
# You will neither create nor use endpoint descendants or instances directly:
|
16
12
|
#
|
17
13
|
# * endpoint class definition is performed through {DSL} helpers,
|
18
|
-
# * and then, containing namespace obtains `.<
|
19
|
-
#
|
14
|
+
# * and then, containing namespace obtains `.<endpoint_name>()` method, which is (almost)
|
15
|
+
# everything you need to know.
|
20
16
|
#
|
21
17
|
class Endpoint < APIPath
|
22
18
|
class << self
|
19
|
+
# @private
|
20
|
+
attr_reader :processors
|
21
|
+
|
23
22
|
# Inspects endpoint class prettily.
|
24
23
|
#
|
25
24
|
# Example:
|
26
25
|
#
|
27
26
|
# ```ruby
|
28
|
-
# some_api.some_namespace.
|
27
|
+
# some_api.some_namespace.endpoint(:my_endpoint)
|
29
28
|
# # => <SomeApi::SomeNamespace::MyEndpoint call-sequence: my_endpoint(param1, param2: nil), docs: .describe>
|
30
29
|
# ```
|
30
|
+
#
|
31
|
+
# @return [String]
|
31
32
|
def inspect
|
32
|
-
|
33
|
-
" call-sequence: #{symbol}(#{param_set.to_code}); docs: .describe>"
|
34
|
-
end
|
33
|
+
return super unless is_defined?
|
35
34
|
|
36
|
-
|
37
|
-
def to_code
|
38
|
-
"def #{to_method_definition}\n" \
|
39
|
-
" child(:#{symbol}, Endpoint).call({#{param_set.to_hash_code}})\n" \
|
40
|
-
'end'
|
35
|
+
Formatting::Inspect.endpoint_class(self)
|
41
36
|
end
|
42
37
|
|
43
|
-
# @
|
44
|
-
def
|
45
|
-
|
46
|
-
base_url
|
47
|
-
else
|
48
|
-
joiner = base_url.include?('?') ? '&' : '?'
|
49
|
-
"#{base_url}{#{joiner}#{query_string_params.join(',')}}"
|
50
|
-
end
|
51
|
-
Addressable::Template.new(tpl)
|
52
|
-
end
|
38
|
+
# @return [Formatting::Description]
|
39
|
+
def describe
|
40
|
+
return '' unless is_defined?
|
53
41
|
|
54
|
-
|
55
|
-
def parse(body)
|
56
|
-
if xml
|
57
|
-
Crack::XML.parse(body)
|
58
|
-
else
|
59
|
-
JSON.parse(body)
|
60
|
-
end.derp { |response| response_processor.process(response) }
|
42
|
+
Formatting::Describe.endpoint_class(self)
|
61
43
|
end
|
62
44
|
|
63
|
-
|
45
|
+
protected
|
64
46
|
|
65
|
-
def
|
66
|
-
|
67
|
-
|
47
|
+
def setup(processors: [], **args)
|
48
|
+
super(**args)
|
49
|
+
self.processors = processors.dup
|
68
50
|
end
|
51
|
+
|
52
|
+
attr_writer :processors
|
69
53
|
end
|
70
54
|
|
71
|
-
|
55
|
+
# @private
|
56
|
+
attr_reader :url, :request_params
|
72
57
|
|
73
|
-
# Creates endpoint class (or descendant) instance. Typically, you
|
74
|
-
# never use it directly.
|
58
|
+
# Creates endpoint class (or descendant) instance. Typically, you never use it directly.
|
75
59
|
#
|
76
60
|
# Params defined in parent namespace are passed here.
|
77
61
|
#
|
78
|
-
def initialize(**
|
62
|
+
def initialize(parent, **params)
|
79
63
|
super
|
80
64
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
65
|
+
template = Addressable::Template.new(url_template)
|
66
|
+
|
67
|
+
# .normalize fixes ../ in path
|
68
|
+
@url = template.expand(**prepared_params).normalize.to_s.yield_self(&method(:fix_slash))
|
69
|
+
url_keys = template.keys.map(&:to_sym)
|
70
|
+
@request_params = prepared_params.reject { |k,| url_keys.include?(k) }
|
86
71
|
end
|
87
72
|
|
88
|
-
# Does the real call to the API, with all params passed to this method
|
89
|
-
# and to parent namespace.
|
73
|
+
# Does the real call to the API, with all params passed to this method and to parent namespace.
|
90
74
|
#
|
91
|
-
# Typically, you don't use it directly, that's what called when you
|
92
|
-
#
|
75
|
+
# Typically, you don't use it directly, that's what called when you do
|
76
|
+
# `some_namespace.endpoint_name(**params)`.
|
93
77
|
#
|
94
|
-
# @return [Hash,Array] Parsed, flattened and post-processed response
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
@client.get(url)
|
100
|
-
.tap { |response| guard_errors!(response) }
|
101
|
-
.derp { |response| self.class.parse(response.body) }
|
102
|
-
rescue API::Error
|
103
|
-
raise # Not catching in the next block
|
104
|
-
rescue => e
|
105
|
-
raise unless url
|
106
|
-
raise API::Error, "#{e.class} at #{url}: #{e.message}"
|
78
|
+
# @return [Hash,Array] Parsed, flattened and post-processed response body.
|
79
|
+
def call
|
80
|
+
# TODO: we have a whole response here, so we can potentially have processors that
|
81
|
+
# extract some useful information (pagination, rate-limiting) from _headers_.
|
82
|
+
api.request(url, **request_params).body.yield_self(&method(:parse))
|
107
83
|
end
|
108
84
|
|
109
|
-
|
110
|
-
|
111
|
-
|
85
|
+
# @return [String]
|
86
|
+
def inspect
|
87
|
+
Formatting::Inspect.endpoint(self)
|
88
|
+
end
|
112
89
|
|
113
|
-
|
114
|
-
|
90
|
+
# @private
|
91
|
+
def to_curl
|
92
|
+
separator = url.include?('?') ? '&' : '?'
|
93
|
+
full_url = url + separator + request_params.map(&'%s=%s'.method(:%)).join('&')
|
94
|
+
# FIXME: Probably unreliable (escaping), but Shellwords.escape do the wrong thing.
|
95
|
+
%{curl "#{full_url}"}
|
115
96
|
end
|
116
97
|
|
117
|
-
|
118
|
-
# TODO: follow redirects
|
119
|
-
return response if (200...400).cover?(response.status)
|
98
|
+
private
|
120
99
|
|
121
|
-
|
122
|
-
message = body && (body['message'] || body['error'])
|
100
|
+
def_delegators :self_class, :url_template, :processors
|
123
101
|
|
124
|
-
|
125
|
-
|
126
|
-
|
102
|
+
# Fix params substitution: if it was in path part, we shouldn't have escaped "/"
|
103
|
+
# E.g. for template `http://google.com/{foo}/bar`, and foo="some/path", Addressable would
|
104
|
+
# produce "http://google.com/some%2fpath/bar", but we want "http://google.com/some/path/bar"
|
105
|
+
def fix_slash(url)
|
106
|
+
url, query = url.split('?', 2)
|
107
|
+
url.gsub!('%2F', '/')
|
108
|
+
[url, query].compact.join('?')
|
127
109
|
end
|
128
110
|
|
129
|
-
def
|
130
|
-
|
131
|
-
@url_template
|
132
|
-
.expand(url_params).normalize.to_s
|
133
|
-
.split('?', 2).derp { |url, param| [url.gsub('%2F', '/'), param] }
|
134
|
-
.compact.join('?')
|
111
|
+
def parse(body)
|
112
|
+
processors.inject(body) { |res, proc| proc.(res) }
|
135
113
|
end
|
136
114
|
end
|
137
115
|
end
|