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
data/lib/tlaw/param.rb CHANGED
@@ -1,155 +1,59 @@
1
- module TLAW
2
- # Base parameter class for working with parameters validation and
3
- # converting. You'll never instantiate it directly, just see {DSL#param}
4
- # for parameters definition.
5
- #
6
- class Param
7
- # This error is thrown when some value could not be converted to what
8
- # this parameter inspects. For example:
9
- #
10
- # ```ruby
11
- # # definition:
12
- # param :timestamp, :to_time, format: :to_i
13
- # # this means: parameter, when passed, will first be converted with
14
- # # method #to_time, and then resulting time will be made into
15
- # # unix timestamp with #to_i before passing to API
16
- #
17
- # # usage:
18
- # my_endpoint(timestamp: Time.now) # ok
19
- # my_endpoint(timestamp: Date.today) # ok
20
- # my_endpoint(timestamp: '2016-06-01') # Nonconvertible! ...unless you've included ActiveSupport :)
21
- # ```
22
- #
23
- Nonconvertible = Class.new(ArgumentError)
24
-
25
- def self.make(name, **options)
26
- # NB: Sic. :keyword is nil (not provided) should still
27
- # make a keyword argument.
28
- if options[:keyword] != false
29
- KeywordParam.new(name, **options)
30
- else
31
- ArgumentParam.new(name, **options)
32
- end
33
- end
1
+ # frozen_string_literal: true
34
2
 
35
- attr_reader :name, :type, :options
3
+ require_relative 'param/type'
36
4
 
37
- def initialize(name, **options)
5
+ module TLAW
6
+ # @private
7
+ class Param
8
+ attr_reader :name, :field, :type, :description, :default, :format
9
+
10
+ def initialize(
11
+ name:,
12
+ field: name,
13
+ type: nil,
14
+ description: nil,
15
+ required: false,
16
+ keyword: true,
17
+ default: nil,
18
+ format: :itself
19
+ )
38
20
  @name = name
39
- @options = options
40
- @type = Type.parse(options)
41
- @options[:desc] ||= @options[:description]
42
- @options[:desc].gsub!(/\n( *)/, "\n ") if @options[:desc]
43
- @formatter = make_formatter
44
- end
45
-
46
- def required?
47
- options[:required]
48
- end
49
-
50
- def default
51
- options[:default]
52
- end
53
-
54
- def merge(**new_options)
55
- Param.make(name, @options.merge(new_options))
56
- end
57
-
58
- def field
59
- options[:field] || name
60
- end
61
-
62
- def convert(value)
63
- type.convert(value)
64
- end
65
-
66
- def format(value)
67
- to_url_part(formatter.call(value))
68
- end
69
-
70
- def convert_and_format(value)
71
- format(convert(value))
72
- end
73
-
74
- alias_method :to_h, :options
75
-
76
- def description
77
- options[:desc]
78
- end
79
-
80
- def describe
81
- [
82
- '@param', name,
83
- ("[#{doc_type}]" if doc_type),
84
- description,
85
- if @options[:enum]
86
- "\n Possible values: #{type.values.map(&:inspect).join(', ')}"
87
- end,
88
- ("(default = #{default.inspect})" if default)
89
- ].compact.join(' ')
90
- .derp(&Util::Description.method(:new))
91
- end
92
-
93
- private
94
-
95
- attr_reader :formatter
96
-
97
- def doc_type
98
- type.to_doc_type
99
- end
100
-
101
- def to_url_part(value)
102
- case value
103
- when Array
104
- value.join(',')
105
- else
106
- value.to_s
107
- end
108
- end
109
-
110
- def make_formatter
111
- options[:format].derp { |f|
112
- return ->(v) { v } unless f
113
- return f.to_proc if f.respond_to?(:to_proc)
114
- fail ArgumentError, "#{self}: unsupporter formatter #{f}"
21
+ @field = field
22
+ @type = Type.coerce(type)
23
+ @description = description
24
+ @required = required
25
+ @keyword = keyword
26
+ @default = default
27
+ @format = format
28
+ end
29
+
30
+ def to_h
31
+ {
32
+ name: name,
33
+ field: field,
34
+ type: type,
35
+ description: description,
36
+ required: required?,
37
+ keyword: keyword?,
38
+ default: default,
39
+ format: format
115
40
  }
116
41
  end
117
42
 
118
- def default_to_code
119
- # FIXME: this `inspect` will fail with, say, Time
120
- default.inspect
121
- end
122
- end
123
-
124
- # @private
125
- class ArgumentParam < Param
126
- def keyword?
127
- false
128
- end
129
-
130
- def to_code
131
- if required?
132
- name.to_s
133
- else
134
- "#{name}=#{default_to_code}"
135
- end
43
+ def required?
44
+ @required
136
45
  end
137
- end
138
46
 
139
- # @private
140
- class KeywordParam < Param
141
47
  def keyword?
142
- true
48
+ @keyword
143
49
  end
144
50
 
145
- def to_code
146
- if required?
147
- "#{name}:"
148
- else
149
- "#{name}: #{default_to_code}"
150
- end
51
+ def call(value)
52
+ type.(value)
53
+ .yield_self(&format)
54
+ .yield_self { |val| {field => Array(val).join(',')} }
55
+ rescue TypeError => e
56
+ raise TypeError, "#{name}: #{e.message}"
151
57
  end
152
58
  end
153
59
  end
154
-
155
- require_relative 'param/type'
@@ -1,15 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module TLAW
2
4
  class Param
3
5
  # @private
4
6
  class Type
5
7
  attr_reader :type
6
8
 
7
- def self.parse(options)
8
- type = options[:type]
9
+ def self.default_type
10
+ @default_type ||= Type.new(nil)
11
+ end
9
12
 
13
+ def self.coerce(type = nil)
10
14
  case type
11
15
  when nil
12
- options[:enum] ? EnumType.new(options[:enum]) : Type.new(nil)
16
+ default_type
17
+ when Type
18
+ type
13
19
  when Class
14
20
  ClassType.new(type)
15
21
  when Symbol
@@ -17,7 +23,7 @@ module TLAW
17
23
  when Hash
18
24
  EnumType.new(type)
19
25
  else
20
- fail ArgumenError, "Undefined type #{type}"
26
+ fail ArgumentError, "Undefined type #{type}"
21
27
  end
22
28
  end
23
29
 
@@ -25,88 +31,69 @@ module TLAW
25
31
  @type = type
26
32
  end
27
33
 
28
- def to_doc_type
29
- nil
34
+ def ==(other)
35
+ other.is_a?(self.class) && other.type == type
30
36
  end
31
37
 
32
- def convert(value)
33
- validate(value) && _convert(value)
38
+ def call(value)
39
+ validation_error(value)
40
+ &.yield_self { |msg|
41
+ fail TypeError, "expected #{msg}, got #{value.inspect}"
42
+ }
43
+ _convert(value)
34
44
  end
35
45
 
36
- def validate(_value)
37
- true
46
+ private
47
+
48
+ def validation_error(_value)
49
+ nil
38
50
  end
39
51
 
40
52
  def _convert(value)
41
53
  value
42
54
  end
43
-
44
- def nonconvertible!(value, reason)
45
- fail Nonconvertible,
46
- "#{self} can't convert #{value.inspect}: #{reason}"
47
- end
48
55
  end
49
56
 
50
57
  # @private
51
58
  class ClassType < Type
52
- def validate(value)
53
- value.is_a?(type) or
54
- nonconvertible!(value, "not an instance of #{type}")
59
+ private
60
+
61
+ def validation_error(value)
62
+ "instance of #{type}" unless value.is_a?(type)
55
63
  end
56
64
 
57
65
  def _convert(value)
58
66
  value
59
67
  end
60
-
61
- def to_doc_type
62
- type.name
63
- end
64
68
  end
65
69
 
66
70
  # @private
67
71
  class DuckType < Type
72
+ private
73
+
68
74
  def _convert(value)
69
75
  value.send(type)
70
76
  end
71
77
 
72
- def validate(value)
73
- value.respond_to?(type) or
74
- nonconvertible!(value, "not responding to #{type}")
75
- end
76
-
77
- def to_doc_type
78
- "##{type}"
78
+ def validation_error(value)
79
+ "object responding to ##{type}" unless value.respond_to?(type)
79
80
  end
80
81
  end
81
82
 
82
83
  # @private
83
84
  class EnumType < Type
84
- def initialize(enum)
85
- @type =
86
- case enum
87
- when Hash
88
- enum
89
- when ->(e) { e.respond_to?(:map) }
90
- enum.map { |n| [n, n] }.to_h
91
- else
92
- fail ArgumentError, "Unparseable enum: #{enum.inspect}"
93
- end
85
+ def possible_values
86
+ type.keys.map(&:inspect).join(', ')
94
87
  end
95
88
 
96
- def values
97
- type.keys
98
- end
89
+ private
99
90
 
100
- def validate(value)
101
- type.key?(value) or
102
- nonconvertible!(
103
- value,
104
- "is not one of #{type.keys.map(&:inspect).join(', ')}"
105
- )
91
+ def validation_error(value)
92
+ "one of #{possible_values}" unless type.key?(value)
106
93
  end
107
94
 
108
95
  def _convert(value)
109
- type[value]
96
+ type.fetch(value)
110
97
  end
111
98
  end
112
99
  end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TLAW
4
+ # @private
5
+ module ResponseProcessors
6
+ # @private
7
+ module Generators
8
+ module_function
9
+
10
+ def mutate(&block)
11
+ proc { |hash| hash.tap(&block) }
12
+ end
13
+
14
+ def transform_by_key(key_pattern, &block)
15
+ proc { |hash| ResponseProcessors.transform_by_key(hash, key_pattern, &block) }
16
+ end
17
+
18
+ def transform_nested(key_pattern, nested_key_pattern = nil, &block)
19
+ transformer = if nested_key_pattern
20
+ transform_by_key(nested_key_pattern, &block)
21
+ else
22
+ mutate(&block)
23
+ end
24
+ proc { |hash| ResponseProcessors.transform_nested(hash, key_pattern, &transformer) }
25
+ end
26
+ end
27
+
28
+ class << self
29
+ def transform_by_key(value, key_pattern)
30
+ return value unless value.is_a?(Hash)
31
+
32
+ value
33
+ .map { |k, v| key_pattern === k ? [k, yield(v)] : [k, v] } # rubocop:disable Style/CaseEquality
34
+ .to_h
35
+ end
36
+
37
+ def transform_nested(value, key_pattern, &block)
38
+ transform_by_key(value, key_pattern) { |v| v.is_a?(Array) ? v.map(&block) : v }
39
+ end
40
+
41
+ def flatten(value)
42
+ case value
43
+ when Hash
44
+ flatten_hash(value)
45
+ when Array
46
+ value.map(&method(:flatten))
47
+ else
48
+ value
49
+ end
50
+ end
51
+
52
+ def datablize(value)
53
+ case value
54
+ when Hash
55
+ value.transform_values(&method(:datablize))
56
+ when Array
57
+ if !value.empty? && value.all?(Hash)
58
+ DataTable.new(value)
59
+ else
60
+ value
61
+ end
62
+ else
63
+ value
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def flatten_hash(hash)
70
+ hash.flat_map do |k, v|
71
+ v = flatten(v)
72
+ if v.is_a?(Hash)
73
+ v.map { |k1, v1| ["#{k}.#{k1}", v1] }
74
+ else
75
+ [[k, v]]
76
+ end
77
+ end.reject { |_, v| v.nil? }.to_h
78
+ end
79
+ end
80
+ end
81
+ end
data/lib/tlaw/util.rb CHANGED
@@ -1,45 +1,28 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module TLAW
4
+ # @private
2
5
  module Util
3
6
  module_function
4
7
 
5
8
  def camelize(string)
6
- string
7
- .sub(/^[a-z\d]*/, &:capitalize)
8
- .gsub(%r{(?:_|(/))([a-z\d]*)}i) {
9
- "#{$1}#{$2.capitalize}" # rubocop:disable Style/PerlBackrefs
10
- }
9
+ string.sub(/^[a-z\d]*/, &:capitalize)
11
10
  end
12
11
 
13
- # Description is just a String subclass with rewritten `inspect`
14
- # implementation (useful in `irb`/`pry`):
15
- #
16
- # ```ruby
17
- # str = "Description of endpoint:\nIt has params:..."
18
- # # "Description of endpoint:\nIt has params:..."
19
- #
20
- # TLAW::Util::Description.new(str)
21
- # # Description of endpoint:
22
- # # It has params:...
23
- # ```
24
- #
25
- # TLAW uses it when responds to {APIPath.describe}.
26
- #
27
- class Description < String
28
- alias_method :inspect, :to_s
29
-
30
- def initialize(str)
31
- super(str.to_s.gsub(/ +\n/, "\n"))
32
- end
33
-
34
- # @private
35
- def indent(indentation = ' ')
36
- gsub(/(\A|\n)/, '\1' + indentation)
37
- end
12
+ def deindent(string)
13
+ string
14
+ .gsub(/^[ \t]+/, '') # first, remove spaces at a beginning of each line
15
+ .gsub(/\A\n|\n\s*\Z/, '') # then, remove empty lines before and after docs block
16
+ end
38
17
 
39
- # @private
40
- def +(other)
41
- self.class.new(super)
18
+ # Returns [parent, parent.parent, ...]
19
+ def parents(obj)
20
+ result = []
21
+ cursor = obj
22
+ while (cursor = cursor.parent)
23
+ result << cursor
42
24
  end
25
+ result
43
26
  end
44
27
  end
45
28
  end