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