tlaw 0.0.1
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.
- checksums.yaml +7 -0
- data/.codeclimate.yml +11 -0
- data/.yardopts +2 -0
- data/LICENSE.txt +22 -0
- data/README.md +438 -0
- data/examples/demo_base.rb +10 -0
- data/examples/forecast_io.rb +113 -0
- data/examples/forecast_io_demo.rb +72 -0
- data/examples/open_weather_map.rb +266 -0
- data/examples/open_weather_map_demo.rb +219 -0
- data/examples/tmdb_demo.rb +133 -0
- data/examples/urbandictionary_demo.rb +105 -0
- data/lib/tlaw.rb +67 -0
- data/lib/tlaw/api.rb +58 -0
- data/lib/tlaw/api_path.rb +137 -0
- data/lib/tlaw/data_table.rb +116 -0
- data/lib/tlaw/dsl.rb +511 -0
- data/lib/tlaw/endpoint.rb +132 -0
- data/lib/tlaw/namespace.rb +159 -0
- data/lib/tlaw/param.rb +155 -0
- data/lib/tlaw/param/type.rb +113 -0
- data/lib/tlaw/param_set.rb +111 -0
- data/lib/tlaw/response_processor.rb +124 -0
- data/lib/tlaw/util.rb +45 -0
- data/lib/tlaw/version.rb +7 -0
- data/tlaw.gemspec +53 -0
- metadata +265 -0
data/lib/tlaw.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
require 'json'
|
3
|
+
require 'addressable'
|
4
|
+
|
5
|
+
# Let no one know! But they in Ruby committee just too long to add
|
6
|
+
# something like this to the language.
|
7
|
+
#
|
8
|
+
# See also https://bugs.ruby-lang.org/issues/12760
|
9
|
+
#
|
10
|
+
# @private
|
11
|
+
class Object
|
12
|
+
def derp
|
13
|
+
yield self
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# TLAW is a framework for creating API wrappers for get-only APIs (like
|
18
|
+
# weather, geonames and so on) or subsets of APIs (like getting data from
|
19
|
+
# Twitter).
|
20
|
+
#
|
21
|
+
# Short example:
|
22
|
+
#
|
23
|
+
# ```ruby
|
24
|
+
# # Definition:
|
25
|
+
# class OpenWeatherMap < TLAW::API
|
26
|
+
# param :appid, required: true
|
27
|
+
#
|
28
|
+
# namespace :current, '/weather' do
|
29
|
+
# endpoint :city, '?q={city}{,country_code}' do
|
30
|
+
# param :city, required: true
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# # Usage:
|
36
|
+
# api = OpenWeatherMap.new(appid: '<yourappid>')
|
37
|
+
# api.current.weather('Kharkiv')
|
38
|
+
# # => {"weather.main"=>"Clear",
|
39
|
+
# # "weather.description"=>"clear sky",
|
40
|
+
# # "main.temp"=>8,
|
41
|
+
# # "main.pressure"=>1016,
|
42
|
+
# # "main.humidity"=>81,
|
43
|
+
# # "dt"=>2016-09-19 08:30:00 +0300,
|
44
|
+
# # ...}
|
45
|
+
#
|
46
|
+
# ```
|
47
|
+
#
|
48
|
+
# Refer to [README](./file/README.md) for reasoning about why you need it and links to
|
49
|
+
# more detailed demos, or start reading YARD docs from {API} and {DSL}
|
50
|
+
# modules.
|
51
|
+
module TLAW
|
52
|
+
end
|
53
|
+
|
54
|
+
require_relative 'tlaw/util'
|
55
|
+
require_relative 'tlaw/data_table'
|
56
|
+
|
57
|
+
require_relative 'tlaw/param'
|
58
|
+
require_relative 'tlaw/param_set'
|
59
|
+
|
60
|
+
require_relative 'tlaw/api_path'
|
61
|
+
require_relative 'tlaw/endpoint'
|
62
|
+
require_relative 'tlaw/namespace'
|
63
|
+
require_relative 'tlaw/api'
|
64
|
+
|
65
|
+
require_relative 'tlaw/response_processor'
|
66
|
+
|
67
|
+
require_relative 'tlaw/dsl'
|
data/lib/tlaw/api.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
module TLAW
|
2
|
+
# API is just a top-level {Namespace}.
|
3
|
+
#
|
4
|
+
# Basically, you start creating your endpoint by descending from API
|
5
|
+
# and defining namespaces and endpoints through a {DSL} like this:
|
6
|
+
#
|
7
|
+
# ```ruby
|
8
|
+
# class MyCoolAPI < TLAW::API
|
9
|
+
# define do
|
10
|
+
# base 'http://api.mycool.com'
|
11
|
+
#
|
12
|
+
# namespace :awesome do
|
13
|
+
# # ...and so on
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
# ```
|
18
|
+
#
|
19
|
+
# And then, you use it:
|
20
|
+
#
|
21
|
+
# ```ruby
|
22
|
+
# api = MyCoolAPI.new
|
23
|
+
# api.awesome.cool(param: 'value')
|
24
|
+
# ```
|
25
|
+
#
|
26
|
+
# See {DSL} for explanation of API definition, {Namespace} for explanation
|
27
|
+
# of possible usages and {Endpoint} for real calls performing.
|
28
|
+
#
|
29
|
+
class API < Namespace
|
30
|
+
# Thrown when there are an error during call. Contains real URL which
|
31
|
+
# was called at the time of an error.
|
32
|
+
class Error < RuntimeError
|
33
|
+
end
|
34
|
+
|
35
|
+
class << self
|
36
|
+
# Runs the {DSL} inside your API wrapper class.
|
37
|
+
def define(&block)
|
38
|
+
DSL::APIWrapper.new(self).define(&block)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns detailed description of an API, like this:
|
42
|
+
#
|
43
|
+
# ```ruby
|
44
|
+
# MyCoolAPI.describe
|
45
|
+
# # MyCoolAPI.new()
|
46
|
+
# # This is cool API.
|
47
|
+
# #
|
48
|
+
# # Namespaces:
|
49
|
+
# # .awesome()
|
50
|
+
# # This is awesome.
|
51
|
+
# ```
|
52
|
+
#
|
53
|
+
def describe(*)
|
54
|
+
super.sub(/\A./, '')
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module TLAW
|
4
|
+
# Base class for all API pathes: entire API, namespaces and endpoints.
|
5
|
+
# Allows to define params and post-processors on any level.
|
6
|
+
#
|
7
|
+
class APIPath
|
8
|
+
class << self
|
9
|
+
# @private
|
10
|
+
attr_accessor :base_url, :path, :xml, :docs_link
|
11
|
+
|
12
|
+
# @private
|
13
|
+
def symbol
|
14
|
+
# FIXME: the second part is necessary only for describes,
|
15
|
+
# and probably should not be here.
|
16
|
+
@symbol || (name && "#{name}.new")
|
17
|
+
end
|
18
|
+
|
19
|
+
# @private
|
20
|
+
CLASS_NAMES = {
|
21
|
+
:[] => 'Element'
|
22
|
+
}.freeze
|
23
|
+
|
24
|
+
# @private
|
25
|
+
def class_name
|
26
|
+
# TODO:
|
27
|
+
# * validate if it is classifiable
|
28
|
+
# * provide additional option for non-default class name
|
29
|
+
CLASS_NAMES[symbol] || Util.camelize(symbol.to_s)
|
30
|
+
end
|
31
|
+
|
32
|
+
# @private
|
33
|
+
def description=(descr)
|
34
|
+
@description = Util::Description.new(descr)
|
35
|
+
end
|
36
|
+
|
37
|
+
# @private
|
38
|
+
def description
|
39
|
+
return unless @description || @docs_link
|
40
|
+
|
41
|
+
Util::Description.new(
|
42
|
+
[@description, ("Docs: #{@docs_link}" if @docs_link)]
|
43
|
+
.compact.join("\n\n")
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
# @private
|
48
|
+
def inherit(namespace, **attrs)
|
49
|
+
Class.new(self).tap do |subclass|
|
50
|
+
attrs.each { |a, v| subclass.send("#{a}=", v) }
|
51
|
+
namespace.const_set(subclass.class_name, subclass)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# @private
|
56
|
+
def params_from_path!
|
57
|
+
Addressable::Template.new(path).keys.each do |key|
|
58
|
+
param_set.add key.to_sym, keyword: false
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# @private
|
63
|
+
def setup_parents(parent)
|
64
|
+
param_set.parent = parent.param_set
|
65
|
+
response_processor.parent = parent.response_processor
|
66
|
+
end
|
67
|
+
|
68
|
+
# @private
|
69
|
+
def symbol=(sym)
|
70
|
+
@symbol = sym
|
71
|
+
@path ||= "/#{sym}"
|
72
|
+
end
|
73
|
+
|
74
|
+
# @return [ParamSet]
|
75
|
+
def param_set
|
76
|
+
@param_set ||= ParamSet.new
|
77
|
+
end
|
78
|
+
|
79
|
+
# @private
|
80
|
+
def response_processor
|
81
|
+
@response_processor ||= ResponseProcessor.new
|
82
|
+
end
|
83
|
+
|
84
|
+
# @private
|
85
|
+
def to_method_definition
|
86
|
+
"#{symbol}(#{param_set.to_code})"
|
87
|
+
end
|
88
|
+
|
89
|
+
# Redefined on descendants, it just allows you to do `api.namespace.describe`
|
90
|
+
# or `api.namespace1.namespace2.endpoints[:my_endpoint].describe`
|
91
|
+
# and have reasonable useful description printed.
|
92
|
+
#
|
93
|
+
# @return [Util::Description] It is just description string but with
|
94
|
+
# redefined `#inspect` to be pretty-printed in console.
|
95
|
+
def describe(definition = nil)
|
96
|
+
Util::Description.new(
|
97
|
+
".#{definition || to_method_definition}" +
|
98
|
+
(description ? "\n" + description.indent(' ') + "\n" : '') +
|
99
|
+
(param_set.empty? ? '' : "\n" + param_set.describe.indent(' '))
|
100
|
+
)
|
101
|
+
end
|
102
|
+
|
103
|
+
# @private
|
104
|
+
def describe_short
|
105
|
+
Util::Description.new(
|
106
|
+
".#{to_method_definition}" +
|
107
|
+
(description ? "\n" + description_first_para.indent(' ') : '')
|
108
|
+
)
|
109
|
+
end
|
110
|
+
|
111
|
+
# @private
|
112
|
+
def define_method_on(host)
|
113
|
+
file, line = method(:to_code).source_location
|
114
|
+
# line + 1 is where real definition, theoretically, starts
|
115
|
+
host.module_eval(to_code, file, line + 1)
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def description_first_para
|
121
|
+
description.split("\n\n").first
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
extend Forwardable
|
126
|
+
|
127
|
+
def initialize(**parent_params)
|
128
|
+
@parent_params = parent_params
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
def object_class
|
134
|
+
self.class
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module TLAW
|
2
|
+
# Basically, just a 2-d array with column names. Or you can think of
|
3
|
+
# it as an array of hashes. Or loose DataFrame implementation.
|
4
|
+
#
|
5
|
+
# Just like this:
|
6
|
+
#
|
7
|
+
# ```ruby
|
8
|
+
# tbl = DataTable.new([
|
9
|
+
# {id: 1, name: 'Mike', salary: 1000},
|
10
|
+
# {id: 2, name: 'Doris', salary: 900},
|
11
|
+
# {id: 3, name: 'Angie', salary: 1200}
|
12
|
+
# ])
|
13
|
+
# # => #<TLAW::DataTable[id, name, salary] x 3>
|
14
|
+
# tbl.count
|
15
|
+
# # => 3
|
16
|
+
# tbl.keys
|
17
|
+
# # => ["id", "name", "salary"]
|
18
|
+
# tbl[0]
|
19
|
+
# # => {"id"=>1, "name"=>"Mike", "salary"=>1000}
|
20
|
+
# tbl['salary']
|
21
|
+
# # => [1000, 900, 1200]
|
22
|
+
# ```
|
23
|
+
#
|
24
|
+
# Basically, that's it. Every array of hashes in TLAW response will be
|
25
|
+
# converted into corresponding `DataTable`.
|
26
|
+
#
|
27
|
+
class DataTable < Array
|
28
|
+
def self.from_columns(column_names, columns)
|
29
|
+
from_rows(column_names, columns.transpose)
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.from_rows(column_names, rows)
|
33
|
+
new rows.map { |r| column_names.zip(r).to_h }
|
34
|
+
end
|
35
|
+
|
36
|
+
# Creates DataTable from array of hashes.
|
37
|
+
#
|
38
|
+
# Note, that all hash keys are stringified, and all hashes are expanded
|
39
|
+
# to have same set of keys.
|
40
|
+
#
|
41
|
+
# @param hashes [Array<Hash>]
|
42
|
+
def initialize(hashes)
|
43
|
+
hashes = hashes.each_with_index.map { |h, i|
|
44
|
+
h.is_a?(Hash) or
|
45
|
+
fail ArgumentError,
|
46
|
+
"All rows are expected to be hashes, row #{i} is #{h.class}"
|
47
|
+
|
48
|
+
h.map { |k, v| [k.to_s, v] }.to_h
|
49
|
+
}
|
50
|
+
empty = hashes.map(&:keys).flatten.uniq.map { |k| [k, nil] }.to_h
|
51
|
+
hashes = hashes.map { |h| empty.merge(h) }
|
52
|
+
super(hashes)
|
53
|
+
end
|
54
|
+
|
55
|
+
# All column names.
|
56
|
+
#
|
57
|
+
# @return [Array<String>]
|
58
|
+
def keys
|
59
|
+
empty? ? [] : first.keys
|
60
|
+
end
|
61
|
+
|
62
|
+
# Allows access to one column or row.
|
63
|
+
#
|
64
|
+
# @overload [](index)
|
65
|
+
# Returns one row from a DataTable.
|
66
|
+
#
|
67
|
+
# @param index [Integer] Row number
|
68
|
+
# @return [Hash] Row as a hash
|
69
|
+
#
|
70
|
+
# @overload [](column_name)
|
71
|
+
# Returns one column from a DataTable.
|
72
|
+
#
|
73
|
+
# @param column_name [String] Name of column
|
74
|
+
# @return [Array] Column as an array of all values in it
|
75
|
+
#
|
76
|
+
def [](index_or_column)
|
77
|
+
case index_or_column
|
78
|
+
when Integer
|
79
|
+
super
|
80
|
+
when String, Symbol
|
81
|
+
map { |h| h[index_or_column.to_s] }
|
82
|
+
else
|
83
|
+
fail ArgumentError,
|
84
|
+
'Expected integer or string/symbol index' \
|
85
|
+
", got #{index_or_column.class}"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Slice of a DataTable with only specified columns left.
|
90
|
+
#
|
91
|
+
# @param names [Array<String>] What columns to leave in a DataTable
|
92
|
+
# @return [DataTable]
|
93
|
+
def columns(*names)
|
94
|
+
names.map!(&:to_s)
|
95
|
+
DataTable.new(map { |h| names.map { |n| [n, h[n]] }.to_h })
|
96
|
+
end
|
97
|
+
|
98
|
+
# Represents DataTable as a `column name => all values in columns`
|
99
|
+
# hash.
|
100
|
+
#
|
101
|
+
# @return [Hash{String => Array}]
|
102
|
+
def to_h
|
103
|
+
keys.map { |k| [k, map { |h| h[k] }] }.to_h
|
104
|
+
end
|
105
|
+
|
106
|
+
# @private
|
107
|
+
def inspect
|
108
|
+
"#<#{self.class.name}[#{keys.join(', ')}] x #{size}>"
|
109
|
+
end
|
110
|
+
|
111
|
+
# @private
|
112
|
+
def pretty_print(pp)
|
113
|
+
pp.text("#<#{self.class.name}[#{keys.join(', ')}] x #{size}>")
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
data/lib/tlaw/dsl.rb
ADDED
@@ -0,0 +1,511 @@
|
|
1
|
+
module TLAW
|
2
|
+
# This module is core of a TLAW API definition. It works like this:
|
3
|
+
#
|
4
|
+
# ```ruby
|
5
|
+
# class MyAPI < TLAW::API
|
6
|
+
# define do # here starts what DSL does
|
7
|
+
# namespace :ns do
|
8
|
+
#
|
9
|
+
# endpoint :es do
|
10
|
+
# param :param1, Integer, default: 1
|
11
|
+
# end
|
12
|
+
# end
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
# ```
|
16
|
+
#
|
17
|
+
# Methods of current namespace documentation describe everything you
|
18
|
+
# can use inside `define` blocks. Actual structure of things is a bit
|
19
|
+
# more complicated (relate to lib/tlaw/dsl.rb if you wish), but current
|
20
|
+
# documentation structure considered to be most informative.
|
21
|
+
#
|
22
|
+
module DSL
|
23
|
+
# @!method base(url)
|
24
|
+
# Allows to set entire API base URL, all endpoints and namespaces
|
25
|
+
# pathes are calculated relative to it.
|
26
|
+
#
|
27
|
+
# **Works for:** API
|
28
|
+
#
|
29
|
+
# @param url [String]
|
30
|
+
|
31
|
+
# @!method desc(text)
|
32
|
+
# Allows to set description string for your API object. It can
|
33
|
+
# be multiline, and TLAW will automatically un-indent excessive
|
34
|
+
# indentations:
|
35
|
+
#
|
36
|
+
# ```ruby
|
37
|
+
# # ...several levels of indents while you create a definition
|
38
|
+
# desc %Q{
|
39
|
+
# This is some endpoint.
|
40
|
+
# And it works!
|
41
|
+
# }
|
42
|
+
#
|
43
|
+
# # ...but when you are using it...
|
44
|
+
# p my_api.endpoints[:endpoint].describe
|
45
|
+
# # This is some endpoint.
|
46
|
+
# # And it works!
|
47
|
+
# # ....
|
48
|
+
# ```
|
49
|
+
#
|
50
|
+
# **Works for:** API, namespace, endpoint
|
51
|
+
#
|
52
|
+
# @param text [String]
|
53
|
+
|
54
|
+
# @!method docs(link)
|
55
|
+
# Allows to add link to documentation as a separate line to
|
56
|
+
# object description. Just to be semantic :)
|
57
|
+
#
|
58
|
+
# ```ruby
|
59
|
+
# # you do something like
|
60
|
+
# desc "That's my endpoint"
|
61
|
+
#
|
62
|
+
# docs "http://docs.example.com/my/endpoint"
|
63
|
+
#
|
64
|
+
# # ...and then somewhere...
|
65
|
+
# p my_api.endpoints[:endpoint].describe
|
66
|
+
# # That is my endpoint.
|
67
|
+
# #
|
68
|
+
# # Docs: http://docs.example.com/my/endpoint
|
69
|
+
# # ....
|
70
|
+
# ```
|
71
|
+
#
|
72
|
+
# **Works for:** API, namespace, endpoint
|
73
|
+
#
|
74
|
+
# @param link [String]
|
75
|
+
|
76
|
+
# @!method param(name, type = nil, keyword: true, required: false, **opts)
|
77
|
+
# Defines parameter for current API (global), namespace or endpoint.
|
78
|
+
#
|
79
|
+
# Param defnition defines several things:
|
80
|
+
#
|
81
|
+
# * how method definition to call this namespace/endpoint would
|
82
|
+
# look like: whether the parameter is keyword or regular argument,
|
83
|
+
# whether it is required and what is default value otherwise;
|
84
|
+
# * how parameter is processed: converted and validated from passed
|
85
|
+
# value;
|
86
|
+
# * how param is sent to target API: how it will be called in
|
87
|
+
# the query string and formatted on call.
|
88
|
+
#
|
89
|
+
# Note also those things about params:
|
90
|
+
#
|
91
|
+
# * as described in {#namespace} and {#endpoint}, setting path template
|
92
|
+
# will implicitly set params. You can rewrite this on implicit
|
93
|
+
# param call, for ex:
|
94
|
+
#
|
95
|
+
# ```ruby
|
96
|
+
# endpoint :foo, '/foo/{bar}'
|
97
|
+
# # call-sequence would be foo(bar = nil)
|
98
|
+
#
|
99
|
+
# # But you can make it back keyword:
|
100
|
+
# endpoint :foo, '/foo/{bar}' do
|
101
|
+
# param :bar, keyword: true, default: 'test'
|
102
|
+
# end
|
103
|
+
# # call-sequence now is foo(bar: 'test')
|
104
|
+
#
|
105
|
+
# # Or make it strictly required
|
106
|
+
# endpoint :foo, '/foo/{bar}/{baz}' do
|
107
|
+
# param :bar, required: true
|
108
|
+
# param :baz, keyword: true, required: true
|
109
|
+
# end
|
110
|
+
# # call-sequence now is foo(bar, baz:)
|
111
|
+
# ```
|
112
|
+
#
|
113
|
+
# * param of outer namespace are passed to API on call from inner
|
114
|
+
# namespaces and endpoints, for ex:
|
115
|
+
#
|
116
|
+
# ```ruby
|
117
|
+
# namespace :city do
|
118
|
+
# param :city_name
|
119
|
+
#
|
120
|
+
# namespace :population do
|
121
|
+
# endpoint :by_year, '/year/{year}'
|
122
|
+
# end
|
123
|
+
# end
|
124
|
+
#
|
125
|
+
# # real call:
|
126
|
+
# api.city('London').population.by_year(2015)
|
127
|
+
# # Will get http://api.example.com/city/year/2015?city_name=London
|
128
|
+
# ```
|
129
|
+
#
|
130
|
+
# **Works for:** API, namespace, endpoint
|
131
|
+
#
|
132
|
+
# @param name [Symbol] Parameter name
|
133
|
+
# @param type [Class, Symbol] Expected parameter type. Could by
|
134
|
+
# some class (then parameter would be checked for being instance
|
135
|
+
# of this class or it would be `ArgumentError`), or duck type
|
136
|
+
# (method name that parameter value should respond to).
|
137
|
+
# @param keyword [true, false] Whether the param will go as a
|
138
|
+
# keyword param to method definition.
|
139
|
+
# @param required [true, false] Whether this param is required.
|
140
|
+
# It will be considered on method definition.
|
141
|
+
# @param opts [Hash] Options
|
142
|
+
# @option opts [Symbol] :field What the field would be called in
|
143
|
+
# API query string (it would be `name` by default).
|
144
|
+
# @option opts [#to_proc] :format How to format this option before
|
145
|
+
# including into URL. By default, it is just `.to_s`.
|
146
|
+
# @option opts [String] :desc Param description. You could do it
|
147
|
+
# multiline and with indents, like {#desc}.
|
148
|
+
# @option opts :default Default value for this param. Would be
|
149
|
+
# rendered in method definition and then passed to target API
|
150
|
+
# _(TODO: in future, there also would be "invisible" params,
|
151
|
+
# that are just passed to target, always the same, as well as
|
152
|
+
# params that aren't passed at all if user gave default value.)_
|
153
|
+
# @option opts [Hash, Array] :enum Whether parameter only accepts
|
154
|
+
# enumerated values. Two forms are accepted:
|
155
|
+
#
|
156
|
+
# ```ruby
|
157
|
+
# # array form
|
158
|
+
# param :units, enum: %i[us metric britain]
|
159
|
+
# # parameter accepts only :us, :metric, :britain values, and
|
160
|
+
# # passes them to target API as is
|
161
|
+
#
|
162
|
+
# # hash "accepted => passed" form
|
163
|
+
# param :compact, enum: {true => 'gzip', false => nil}
|
164
|
+
# # parameter accepts true or false, on true passes "compact=gzip",
|
165
|
+
# # on false passes nothing.
|
166
|
+
# ```
|
167
|
+
|
168
|
+
# @!method namespace(name, path = nil, &block)
|
169
|
+
# Defines new namespace or updates existing one.
|
170
|
+
#
|
171
|
+
# {Namespace} has two roles:
|
172
|
+
#
|
173
|
+
# * on Ruby API, defines how you access to the final endpoint,
|
174
|
+
# like `api.namespace1.namespace2(some_param).endpoint(...)`
|
175
|
+
# * on calling API, it adds its path to entire URL.
|
176
|
+
#
|
177
|
+
# **NB:** If you call `namespace(:something)` and it was already defined,
|
178
|
+
# current definition will be added to existing one (but it can't
|
179
|
+
# change path of existing one, which is reasonable).
|
180
|
+
#
|
181
|
+
# **Works for:** API, namespace
|
182
|
+
#
|
183
|
+
# @param name [Symbol] Name of the method by which namespace would
|
184
|
+
# be accessible.
|
185
|
+
# @param path [String] Path to add to API inside this namespace.
|
186
|
+
# When not provided, considered to be `/<name>`. When provided,
|
187
|
+
# taken literally (no slashes or other symbols added). Note, that
|
188
|
+
# you can use `/../` in path, redesigning someone else's APIs on
|
189
|
+
# the fly. Also, you can use [RFC 6570](https://www.rfc-editor.org/rfc/rfc6570.txt)
|
190
|
+
# URL templates to mark params going straightly into URI.
|
191
|
+
#
|
192
|
+
# Some examples:
|
193
|
+
#
|
194
|
+
# ```ruby
|
195
|
+
# # assuming API base url is http://api.example.com
|
196
|
+
#
|
197
|
+
# namespace :foo
|
198
|
+
# # method would be foo(), API URL would be http://api.example.com/foo
|
199
|
+
#
|
200
|
+
# namespace :bar, '/foo/bar'
|
201
|
+
# # metod would be bar(), API URL http://api.example.com/foo/bar
|
202
|
+
#
|
203
|
+
# namespace :baz, ''
|
204
|
+
# # method baz(), API URL same as base: useful for gathering into
|
205
|
+
# # quazi-namespace from several unrelated endpoints.
|
206
|
+
#
|
207
|
+
# namespace :quux, '/foo/quux/{id}'
|
208
|
+
# # method quux(id = nil), API URL http://api.example.com/foo/quux/123
|
209
|
+
# # ...where 123 is what you've passed as id
|
210
|
+
# ```
|
211
|
+
# @param block Definition of current namespace params, and
|
212
|
+
# namespaces and endpoints inside current.
|
213
|
+
# Note that by defining params inside this block, you can change
|
214
|
+
# namespace's method call sequence.
|
215
|
+
#
|
216
|
+
# For example:
|
217
|
+
#
|
218
|
+
# ```ruby
|
219
|
+
# namespace :foo
|
220
|
+
# # call-sequence: foo()
|
221
|
+
#
|
222
|
+
# namespace :foo do
|
223
|
+
# param :bar
|
224
|
+
# end
|
225
|
+
# # call-sequence: foo(bar: nil)
|
226
|
+
#
|
227
|
+
# namespace :foo do
|
228
|
+
# param :bar, required: true, keyword: false
|
229
|
+
# param :baz, required: true
|
230
|
+
# end
|
231
|
+
# # call-sequence: foo(bar, baz:)
|
232
|
+
# ```
|
233
|
+
#
|
234
|
+
# ...and so on. See also {#param} for understanding what you
|
235
|
+
# can change here.
|
236
|
+
#
|
237
|
+
|
238
|
+
# @!method endpoint(name, path = nil, **opts, &block)
|
239
|
+
# Defines new endpoint or updates existing one.
|
240
|
+
#
|
241
|
+
# {Endpoint} is the thing doing the real work: providing Ruby API
|
242
|
+
# method to really call target API.
|
243
|
+
#
|
244
|
+
# **NB:** If you call `endpoint(:something)` and it was already defined,
|
245
|
+
# current definition will be added to existing one (but it can't
|
246
|
+
# change path of existing one, which is reasonable).
|
247
|
+
#
|
248
|
+
# **Works for:** API, namespace
|
249
|
+
#
|
250
|
+
# @param name [Symbol] Name of the method by which endpoint would
|
251
|
+
# be accessible.
|
252
|
+
# @param path [String] Path to call API from this endpoint.
|
253
|
+
# When not provided, considered to be `/<name>`. When provided,
|
254
|
+
# taken literally (no slashes or other symbols added). Note, that
|
255
|
+
# you can use `/../` in path, redesigning someone else's APIs on
|
256
|
+
# the fly. Also, you can use [RFC 6570](https://www.rfc-editor.org/rfc/rfc6570.txt)
|
257
|
+
# URL templates to mark params going straightly into URI.
|
258
|
+
#
|
259
|
+
# Look at {#namespace} for examples, idea is the same.
|
260
|
+
#
|
261
|
+
# @param opts [Hash] Some options, currently only `:xml`.
|
262
|
+
# @option opts [true, false] :xml Whether endpoint's response should
|
263
|
+
# be parsed as XML (JSON otherwise & by default). Parsing in this
|
264
|
+
# case is performed with [crack](https://github.com/jnunemaker/crack),
|
265
|
+
# producing the hash, to which all other rules of post-processing
|
266
|
+
# are applied.
|
267
|
+
# @param block Definition of endpoint's params and docs.
|
268
|
+
# Note that by defining params inside this block, you can change
|
269
|
+
# endpoints's method call sequence.
|
270
|
+
#
|
271
|
+
# For example:
|
272
|
+
#
|
273
|
+
# ```ruby
|
274
|
+
# endpoint :foo
|
275
|
+
# # call-sequence: foo()
|
276
|
+
#
|
277
|
+
# endpoint :foo do
|
278
|
+
# param :bar
|
279
|
+
# end
|
280
|
+
# # call-sequence: foo(bar: nil)
|
281
|
+
#
|
282
|
+
# endpoint :foo do
|
283
|
+
# param :bar, required: true, keyword: false
|
284
|
+
# param :baz, required: true
|
285
|
+
# end
|
286
|
+
# # call-sequence: foo(bar, baz:)
|
287
|
+
# ```
|
288
|
+
#
|
289
|
+
# ...and so on. See also {#param} for understanding what you
|
290
|
+
# can change here.
|
291
|
+
|
292
|
+
# @!method post_process(key = nil, &block)
|
293
|
+
# Sets post-processors for response.
|
294
|
+
#
|
295
|
+
# There are also {#post_process_replace} (for replacing entire
|
296
|
+
# response with something else) and {#post_process_items} (for
|
297
|
+
# post-processing each item of sub-array).
|
298
|
+
#
|
299
|
+
# Notes:
|
300
|
+
#
|
301
|
+
# * you can set any number of post-processors of any kind, and they
|
302
|
+
# will be applied in exactly the same order they are set;
|
303
|
+
# * you can set post-processors in parent namespace (or for entire
|
304
|
+
# API), in this case post-processors of _outer_ namespace are
|
305
|
+
# always applied before inner ones. That allow you to define some
|
306
|
+
# generic parsing/rewriting on API level, then more specific
|
307
|
+
# key postprocessors on endpoints;
|
308
|
+
# * hashes are flattened again after _each_ post-processor, so if
|
309
|
+
# for some `key` you'll return `{count: 1, continue: false}`,
|
310
|
+
# response hash will immediately have
|
311
|
+
# `{"key.count" => 1, "key.continue" => false}`.
|
312
|
+
#
|
313
|
+
# @overload post_process(&block)
|
314
|
+
# Sets post-processor for whole response. Note, that in this case
|
315
|
+
# _return_ value of block is ignored, it is expected that your
|
316
|
+
# block will receive response and modify it inplace, like this:
|
317
|
+
#
|
318
|
+
# ```ruby
|
319
|
+
# post_process do |response|
|
320
|
+
# response['coord'] = Geo::Coord.new(response['lat'], response['lng'])
|
321
|
+
# end
|
322
|
+
# ```
|
323
|
+
# If you need to replace entire response with something else,
|
324
|
+
# see {#post_process_replace}
|
325
|
+
#
|
326
|
+
# @overload post_process(key, &block)
|
327
|
+
# Sets post-processor for one response key. Post-processor is
|
328
|
+
# called only if key exists in the response, and value by this
|
329
|
+
# key is replaced with post-processor's response.
|
330
|
+
#
|
331
|
+
# Note, that if `block` returns `nil`, key will be removed completely.
|
332
|
+
#
|
333
|
+
# Usage:
|
334
|
+
#
|
335
|
+
# ```ruby
|
336
|
+
# post_process('date') { |val| Date.parse(val) }
|
337
|
+
# # or, btw, just
|
338
|
+
# post_process('date', &Date.method(:parse))
|
339
|
+
# ```
|
340
|
+
#
|
341
|
+
# @param key [String]
|
342
|
+
|
343
|
+
# @!method post_process_items(key, &block)
|
344
|
+
# Sets post-processors for each items of array, being at `key` (if
|
345
|
+
# the key is present in response, and if its value is array of
|
346
|
+
# hashes).
|
347
|
+
#
|
348
|
+
# Inside `block` you can use {#post_process} method as described
|
349
|
+
# above (but all of its actions will be related only to current
|
350
|
+
# item of array).
|
351
|
+
#
|
352
|
+
# Example:
|
353
|
+
#
|
354
|
+
# Considering API response like:
|
355
|
+
#
|
356
|
+
# ```json
|
357
|
+
# {
|
358
|
+
# "meta": {"count": 100},
|
359
|
+
# "data": [
|
360
|
+
# {"timestamp": "2016-05-01", "value": "10", "dummy": "foo"},
|
361
|
+
# {"timestamp": "2016-05-02", "value": "13", "dummy": "bar"}
|
362
|
+
# ]
|
363
|
+
# }
|
364
|
+
# ```
|
365
|
+
# ...you can define postprocessing like this:
|
366
|
+
#
|
367
|
+
# ```ruby
|
368
|
+
# post_process_items 'data' do
|
369
|
+
# post_process 'timestamp', &Date.method(:parse)
|
370
|
+
# post_process 'value', &:to_i
|
371
|
+
# post_process('dummy'){nil} # will be removed
|
372
|
+
# end
|
373
|
+
# ```
|
374
|
+
#
|
375
|
+
# See also {#post_process} for some generic explanation of post-processing.
|
376
|
+
#
|
377
|
+
# @param key [String]
|
378
|
+
|
379
|
+
# @!method post_process_replace(&block)
|
380
|
+
# Just like {#post_process} for entire response, but _replaces_
|
381
|
+
# it with what block returns.
|
382
|
+
#
|
383
|
+
# Real-life usage: WorldBank API typically returns responses this
|
384
|
+
# way:
|
385
|
+
#
|
386
|
+
# ```json
|
387
|
+
# [
|
388
|
+
# {"count": 100, "page": 1},
|
389
|
+
# {"some_data_variable": [{}, {}, {}]}
|
390
|
+
# ]
|
391
|
+
# ```
|
392
|
+
# ...e.g. metadata and real response as two items in array, not
|
393
|
+
# two keys in hash. We can easily fix this:
|
394
|
+
#
|
395
|
+
# ```ruby
|
396
|
+
# post_process_replace do |response|
|
397
|
+
# {meta: response.first, data: response.last}
|
398
|
+
# end
|
399
|
+
# ```
|
400
|
+
#
|
401
|
+
# See also {#post_process} for some generic explanation of post-processing.
|
402
|
+
|
403
|
+
# @private
|
404
|
+
class BaseWrapper
|
405
|
+
def initialize(object)
|
406
|
+
@object = object
|
407
|
+
end
|
408
|
+
|
409
|
+
def define(&block)
|
410
|
+
instance_eval(&block)
|
411
|
+
end
|
412
|
+
|
413
|
+
def description(text)
|
414
|
+
# first, remove spaces at a beginning of each line
|
415
|
+
# then, remove empty lines before and after docs block
|
416
|
+
@object.description =
|
417
|
+
text
|
418
|
+
.gsub(/^[ \t]+/, '')
|
419
|
+
.gsub(/\A\n|\n\s*\Z/, '')
|
420
|
+
end
|
421
|
+
|
422
|
+
alias_method :desc, :description
|
423
|
+
|
424
|
+
def docs(link)
|
425
|
+
@object.docs_link = link
|
426
|
+
end
|
427
|
+
|
428
|
+
def param(name, type = nil, **opts)
|
429
|
+
@object.param_set.add(name, **opts.merge(type: type))
|
430
|
+
end
|
431
|
+
|
432
|
+
def post_process(key = nil, &block)
|
433
|
+
@object.response_processor.add_post_processor(key, &block)
|
434
|
+
end
|
435
|
+
|
436
|
+
def post_process_replace(&block)
|
437
|
+
@object.response_processor.add_replacer(&block)
|
438
|
+
end
|
439
|
+
|
440
|
+
class PostProcessProxy
|
441
|
+
def initialize(parent_key, parent)
|
442
|
+
@parent_key = parent_key
|
443
|
+
@parent = parent
|
444
|
+
end
|
445
|
+
|
446
|
+
def post_process(key = nil, &block)
|
447
|
+
@parent.add_item_post_processor(@parent_key, key, &block)
|
448
|
+
end
|
449
|
+
end
|
450
|
+
|
451
|
+
def post_process_items(key, &block)
|
452
|
+
PostProcessProxy
|
453
|
+
.new(key, @object.response_processor)
|
454
|
+
.instance_eval(&block)
|
455
|
+
end
|
456
|
+
end
|
457
|
+
|
458
|
+
# @private
|
459
|
+
class EndpointWrapper < BaseWrapper
|
460
|
+
end
|
461
|
+
|
462
|
+
# @private
|
463
|
+
class NamespaceWrapper < BaseWrapper
|
464
|
+
def endpoint(name, path = nil, **opts, &block)
|
465
|
+
update_existing(Endpoint, name, path, **opts, &block) ||
|
466
|
+
add_child(Endpoint, name, path: path || "/#{name}", **opts, &block)
|
467
|
+
end
|
468
|
+
|
469
|
+
def namespace(name, path = nil, &block)
|
470
|
+
update_existing(Namespace, name, path, &block) ||
|
471
|
+
add_child(Namespace, name, path: path || "/#{name}", &block)
|
472
|
+
end
|
473
|
+
|
474
|
+
private
|
475
|
+
|
476
|
+
WRAPPERS = {
|
477
|
+
Endpoint => EndpointWrapper,
|
478
|
+
Namespace => NamespaceWrapper
|
479
|
+
}.freeze
|
480
|
+
|
481
|
+
def update_existing(child_class, name, path, **opts, &block)
|
482
|
+
existing = @object.children[name] or return nil
|
483
|
+
existing < child_class or
|
484
|
+
fail ArgumentError, "#{name} is already defined as #{child_class == Endpoint ? 'namespace' : 'endpoint'}, you can't redefine it as #{child_class}"
|
485
|
+
|
486
|
+
!path && opts.empty? or
|
487
|
+
fail ArgumentError, "#{child_class} is already defined, you can't change its path or options"
|
488
|
+
|
489
|
+
WRAPPERS[child_class].new(existing).define(&block) if block
|
490
|
+
end
|
491
|
+
|
492
|
+
def add_child(child_class, name, **opts, &block)
|
493
|
+
@object.add_child(
|
494
|
+
child_class.inherit(@object, symbol: name, **opts)
|
495
|
+
.tap { |c| c.setup_parents(@object) }
|
496
|
+
.tap(&:params_from_path!)
|
497
|
+
.tap { |c|
|
498
|
+
WRAPPERS[child_class].new(c).define(&block) if block
|
499
|
+
}
|
500
|
+
)
|
501
|
+
end
|
502
|
+
end
|
503
|
+
|
504
|
+
# @private
|
505
|
+
class APIWrapper < NamespaceWrapper
|
506
|
+
def base(url)
|
507
|
+
@object.base_url = url
|
508
|
+
end
|
509
|
+
end
|
510
|
+
end
|
511
|
+
end
|