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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -0
  3. data/CHANGELOG.md +18 -2
  4. data/README.md +10 -7
  5. data/examples/demo_base.rb +2 -2
  6. data/examples/experimental/README.md +3 -0
  7. data/examples/experimental/afterthedeadline.rb +22 -0
  8. data/examples/experimental/airvisual.rb +14 -0
  9. data/examples/experimental/apixu.rb +32 -0
  10. data/examples/experimental/bing_maps.rb +18 -0
  11. data/examples/experimental/currencylayer.rb +25 -0
  12. data/examples/experimental/earthquake.rb +29 -0
  13. data/examples/experimental/freegeoip.rb +16 -0
  14. data/examples/experimental/geonames.rb +98 -0
  15. data/examples/experimental/isfdb.rb +17 -0
  16. data/examples/experimental/musicbrainz.rb +27 -0
  17. data/examples/experimental/nominatim.rb +52 -0
  18. data/examples/experimental/omdb.rb +68 -0
  19. data/examples/experimental/open_exchange_rates.rb +36 -0
  20. data/examples/experimental/open_route.rb +27 -0
  21. data/examples/experimental/open_street_map.rb +16 -0
  22. data/examples/experimental/quandl.rb +50 -0
  23. data/examples/experimental/reddit.rb +25 -0
  24. data/examples/experimental/swapi.rb +27 -0
  25. data/examples/experimental/tmdb.rb +53 -0
  26. data/examples/experimental/world_bank.rb +85 -0
  27. data/examples/experimental/world_bank_climate.rb +77 -0
  28. data/examples/experimental/wunderground.rb +66 -0
  29. data/examples/experimental/wunderground_demo.rb +7 -0
  30. data/examples/forecast_io.rb +16 -16
  31. data/examples/giphy.rb +4 -4
  32. data/examples/giphy_demo.rb +1 -1
  33. data/examples/open_weather_map.rb +64 -60
  34. data/examples/open_weather_map_demo.rb +4 -4
  35. data/examples/tmdb_demo.rb +1 -1
  36. data/examples/urbandictionary_demo.rb +2 -2
  37. data/lib/tlaw.rb +14 -15
  38. data/lib/tlaw/api.rb +108 -26
  39. data/lib/tlaw/api_path.rb +86 -87
  40. data/lib/tlaw/data_table.rb +15 -10
  41. data/lib/tlaw/dsl.rb +126 -224
  42. data/lib/tlaw/dsl/api_builder.rb +47 -0
  43. data/lib/tlaw/dsl/base_builder.rb +108 -0
  44. data/lib/tlaw/dsl/endpoint_builder.rb +26 -0
  45. data/lib/tlaw/dsl/namespace_builder.rb +86 -0
  46. data/lib/tlaw/endpoint.rb +63 -85
  47. data/lib/tlaw/formatting.rb +55 -0
  48. data/lib/tlaw/formatting/describe.rb +86 -0
  49. data/lib/tlaw/formatting/inspect.rb +52 -0
  50. data/lib/tlaw/namespace.rb +141 -98
  51. data/lib/tlaw/param.rb +45 -141
  52. data/lib/tlaw/param/type.rb +36 -49
  53. data/lib/tlaw/response_processors.rb +81 -0
  54. data/lib/tlaw/util.rb +16 -33
  55. data/lib/tlaw/version.rb +6 -3
  56. data/tlaw.gemspec +9 -9
  57. metadata +63 -13
  58. data/lib/tlaw/param_set.rb +0 -111
  59. 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
@@ -1,159 +1,202 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module TLAW
2
- # Namespace is basically a container for {Endpoint}s. It allows to
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
- # Typically, as with {Endpoint}, you never create namespace instances
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 SampleAPI < TLAW::API
17
- # # namespace definition:
18
- # namespace :my_ns do
19
- # endpoint :weather
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
- # # usage:
24
- # api = SampleAPI.new
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.namespaces[:my_ns] # => class SampleAPI::MyNS, subclass of namespace
27
- # api.my_ns # => short-living instance of SampleAPI::MyNS
28
- # api.my_ns.weather # => real call to API
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
- def base_url=(url)
35
- @base_url = url
36
-
37
- children.values.each do |c|
38
- c.base_url = base_url + c.path if c.path && !c.base_url
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 as a hash.
93
+ # Lists all current namespace's nested namespaces.
43
94
  #
44
- # @return [Hash{Symbol => Namespace}]
95
+ # @return [Array<Namespace>]
45
96
  def namespaces
46
- children.select { |_k, v| v < Namespace }
97
+ children.grep(Namespace.singleton_class)
47
98
  end
48
99
 
49
- # Lists all current namespace's endpoints as a hash.
100
+ # Returns the namespace's nested namespaces of the requested name.
50
101
  #
51
- # @return [Hash{Symbol => Endpoint}]
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.select { |_k, v| v < Endpoint }
112
+ children.grep(Endpoint.singleton_class)
54
113
  end
55
114
 
56
- # @private
57
- def to_code
58
- "def #{to_method_definition}\n" \
59
- " child(:#{symbol}, Namespace, {#{param_set.to_hash_code}})\n" \
60
- 'end'
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
- "#<#{name || '(unnamed namespace class)'}: " \
65
- "call-sequence: #{symbol}(#{param_set.to_code});" +
66
- inspect_docs
67
- end
125
+ return super unless is_defined? || self < API
68
126
 
69
- # @private
70
- def inspect_docs
71
- inspect_namespaces + inspect_endpoints + ' docs: .describe>'
127
+ Formatting::Inspect.namespace_class(self)
72
128
  end
73
129
 
74
- # @private
75
- def add_child(child)
76
- children[child.symbol] = child
77
-
78
- child.base_url = base_url + child.path if !child.base_url && base_url
130
+ # Detailed namespace documentation.
131
+ #
132
+ # @return [Formatting::Description]
133
+ def describe
134
+ return '' unless is_defined?
79
135
 
80
- child.define_method_on(self)
136
+ Formatting::Describe.namespace_class(self)
81
137
  end
82
138
 
83
139
  # @private
84
- def children
85
- @children ||= {}
140
+ def definition
141
+ super.merge(children: children)
86
142
  end
87
143
 
88
- # Detailed namespace documentation.
89
- #
90
- # See {APIPath.describe} for explanations.
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
- def inspect_namespaces
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
- def inspect_endpoints
105
- return '' if endpoints.empty?
106
- " endpoints: #{endpoints.keys.join(', ')};"
107
- end
154
+ protected
108
155
 
109
- def describe_children
110
- describe_namespaces + describe_endpoints
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 describe_namespaces
114
- return '' if namespaces.empty?
115
-
116
- "\n\n Namespaces:\n\n" + children_description(namespaces)
161
+ def children=(children)
162
+ children.each do |child|
163
+ child_index[child.symbol] = child
164
+ end
117
165
  end
118
166
 
119
- def describe_endpoints
120
- return '' if endpoints.empty?
167
+ private
121
168
 
122
- "\n\n Endpoints:\n\n" + children_description(endpoints)
123
- end
169
+ def validate_class(sym, child_class, expected_class)
170
+ return if child_class&.<(expected_class)
124
171
 
125
- def children_description(children)
126
- children.values.map(&:describe_short)
127
- .map { |cd| cd.indent(' ') }.join("\n\n")
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 :object_class,
132
- :symbol,
133
- :children, :namespaces, :endpoints,
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
- "#<#{symbol}(#{param_set.to_hash_code(@parent_params)})" +
138
- self.class.inspect_docs
184
+ Formatting::Inspect.namespace(self)
139
185
  end
140
186
 
141
- def describe
142
- self.class
143
- .describe("#{symbol}(#{param_set.to_hash_code(@parent_params)})")
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(symbol, expected_class, **params)
149
- children[symbol]
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