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