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