apidiesel 0.1.0
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/.gitignore +11 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +36 -0
- data/Rakefile +1 -0
- data/apidiesel.gemspec +26 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/lib/apidiesel/action.rb +185 -0
- data/lib/apidiesel/api.rb +156 -0
- data/lib/apidiesel/dsl.rb +286 -0
- data/lib/apidiesel/errors.rb +15 -0
- data/lib/apidiesel/handlers/action_response_processor.rb +13 -0
- data/lib/apidiesel/handlers/http_request_helper.rb +38 -0
- data/lib/apidiesel/handlers/mock_response.rb +55 -0
- data/lib/apidiesel/request.rb +40 -0
- data/lib/apidiesel/version.rb +3 -0
- data/lib/apidiesel.rb +11 -0
- metadata +120 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 44ad9f9bd05bf14034e7aeffbe7b261121e6bd36
|
4
|
+
data.tar.gz: 63cc65536628b44a8595f9d124bda911db8c8d9e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2e91ff208c1b6782c7cc1fadb1a2b6dc1ded0dc8e86d701fc333538bea818ce2f0affdf641a8dffa6dc0211028967c8c49b9efc528e58a56f1698b5bd04ab6ca
|
7
|
+
data.tar.gz: af65805859d9cbe02578c7f1dcbd0cd31724a58bee865b06414f95d6fb21043c9028caece1033e2809c1a0c9ce87eebe0fbace5c577543ab2ab3c3527866c78c
|
data/.gitignore
ADDED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
apidiesel
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-2.2.3
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Jan-Christian Föh
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
22
|
+
|
data/README.md
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# Apidiesel
|
2
|
+
|
3
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/apidiesel`. To experiment with that code, run `bin/console` for an interactive prompt.
|
4
|
+
|
5
|
+
TODO: Delete this and the text above, and describe your gem
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'apidiesel'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install apidiesel
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
TODO: Write usage instructions here
|
26
|
+
|
27
|
+
## Development
|
28
|
+
|
29
|
+
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
30
|
+
|
31
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
32
|
+
|
33
|
+
## Contributing
|
34
|
+
|
35
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/apidiesel.
|
36
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/apidiesel.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'apidiesel/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "apidiesel"
|
8
|
+
spec.version = Apidiesel::VERSION
|
9
|
+
spec.authors = ["Jan-Christian Foeh"]
|
10
|
+
spec.email = ["jan@programmanstalt.de"]
|
11
|
+
|
12
|
+
spec.summary = %q{Build API clients through an expressive DSL}
|
13
|
+
spec.homepage = "https://github.com/janfoeh/apidiesel"
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = "exe"
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_runtime_dependency 'activesupport', '>= 4.2.0'
|
22
|
+
spec.add_runtime_dependency 'httpi', '>= 2.4.1'
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.10"
|
25
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
26
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "apidiesel"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,185 @@
|
|
1
|
+
module Apidiesel
|
2
|
+
|
3
|
+
# An abstract base class for API endpoints.
|
4
|
+
class Action
|
5
|
+
extend Dsl
|
6
|
+
|
7
|
+
# accessors for class instance variables
|
8
|
+
# (class-level variables, not shared with subclasses)
|
9
|
+
class << self
|
10
|
+
|
11
|
+
# Hash for storing validation closures. These closures are called with the request
|
12
|
+
# parameters before the request is made and have the opportunity to check and modify them.
|
13
|
+
def parameter_validations
|
14
|
+
@parameter_validations ||= []
|
15
|
+
end
|
16
|
+
|
17
|
+
# Hash for storing filter closures. These closures are called with the received data
|
18
|
+
# after a request is made and have the opportunity to modify or check it before the
|
19
|
+
# data is returned
|
20
|
+
def response_filters
|
21
|
+
@response_filters ||= []
|
22
|
+
end
|
23
|
+
|
24
|
+
def response_formatters
|
25
|
+
@response_formatters ||= []
|
26
|
+
end
|
27
|
+
|
28
|
+
def format_parameters(&block)
|
29
|
+
@parameter_formatter = block
|
30
|
+
end
|
31
|
+
|
32
|
+
def parameter_formatter
|
33
|
+
@parameter_formatter
|
34
|
+
end
|
35
|
+
|
36
|
+
# Combined getter/setter for this actions' endpoint
|
37
|
+
#
|
38
|
+
# @param [String] value
|
39
|
+
def endpoint(value = nil)
|
40
|
+
if value
|
41
|
+
@endpoint = value
|
42
|
+
else
|
43
|
+
@endpoint
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Combined getter/setter for this actions URL
|
48
|
+
#
|
49
|
+
# Falls back to the Api setting if blank.
|
50
|
+
#
|
51
|
+
# @param [String] value
|
52
|
+
def url(value = nil)
|
53
|
+
if value
|
54
|
+
@url = value
|
55
|
+
else
|
56
|
+
@url
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Combined getter/setter for the HTTP method used
|
61
|
+
#
|
62
|
+
# Falls back to the Api setting if blank.
|
63
|
+
#
|
64
|
+
# @param [String] value
|
65
|
+
def http_method(value = nil)
|
66
|
+
if value
|
67
|
+
@http_method = value
|
68
|
+
else
|
69
|
+
@http_method
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
attr_accessor :api
|
75
|
+
|
76
|
+
# Hook method that is called by {Apidiesel::Api} to register this Action on itself.
|
77
|
+
#
|
78
|
+
# Example: when {Apidiesel::Api} calls this method inherited on {Apidiesel::Actions::Foo},
|
79
|
+
# it itself gains a `Apidiesel::Api#foo` instance method to instantiate and call the Foo action.
|
80
|
+
#
|
81
|
+
# Executed in {Apidiesel::Api} through
|
82
|
+
#
|
83
|
+
# Apidiesel::Actions.constants.each do |action|
|
84
|
+
# Apidiesel::Actions.const_get(action).register(self)
|
85
|
+
# end
|
86
|
+
def self.register(caller)
|
87
|
+
caller.class_eval <<-EOT
|
88
|
+
def #{name_as_method}(*args)
|
89
|
+
execute_request(#{name}, *args)
|
90
|
+
end
|
91
|
+
EOT
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
# Returns current class name formatted for use as a method name
|
97
|
+
#
|
98
|
+
# Example: {Apidiesel::Actions::Foo} will return `foo`
|
99
|
+
#
|
100
|
+
# @return [String] the demodulized, underscored name of the current Class
|
101
|
+
def self.name_as_method
|
102
|
+
::ActiveSupport::Inflector.underscore( ::ActiveSupport::Inflector.demodulize(self.name) )
|
103
|
+
end
|
104
|
+
|
105
|
+
public
|
106
|
+
|
107
|
+
# @param [Apidiesel::Api] api A reference to the parent Api object
|
108
|
+
def initialize(api)
|
109
|
+
@api = api
|
110
|
+
end
|
111
|
+
|
112
|
+
# Getter/setter for the parameters to be used for creating the API request. Prefilled
|
113
|
+
# with the `op` action key.
|
114
|
+
#
|
115
|
+
# @return [Hash]
|
116
|
+
def parameters
|
117
|
+
@parameters ||= {}
|
118
|
+
end
|
119
|
+
|
120
|
+
def endpoint
|
121
|
+
self.class.endpoint
|
122
|
+
end
|
123
|
+
|
124
|
+
def url
|
125
|
+
self.class.url || @api.class.url
|
126
|
+
end
|
127
|
+
|
128
|
+
def http_method
|
129
|
+
self.class.http_method || @api.class.http_method
|
130
|
+
end
|
131
|
+
|
132
|
+
# Performs the action-specific input validations on `*args` according to the actions
|
133
|
+
# `expects` block, executes the API request and prepares the data according to the
|
134
|
+
# actions `responds_with` block.
|
135
|
+
#
|
136
|
+
# @param [Hash] *args see specific, non-abstract `Apidiesel::Action`
|
137
|
+
# @return [Apidiesel::Request]
|
138
|
+
def build_request(*args)
|
139
|
+
args = args && args.first.is_a?(Hash) ? args.first : {}
|
140
|
+
|
141
|
+
params = {}
|
142
|
+
|
143
|
+
self.class.parameter_validations.each do |validation|
|
144
|
+
validation.call(args, params)
|
145
|
+
end
|
146
|
+
|
147
|
+
if self.class.parameter_formatter
|
148
|
+
params = self.class.parameter_formatter.call(params)
|
149
|
+
end
|
150
|
+
|
151
|
+
Apidiesel::Request.new action: self, parameters: params
|
152
|
+
end
|
153
|
+
|
154
|
+
def process_response(response_hash)
|
155
|
+
processed_result = {}
|
156
|
+
|
157
|
+
response_hash.symbolize_keys!
|
158
|
+
|
159
|
+
if self.class.response_filters.none? && self.class.response_formatters.none?
|
160
|
+
return response_hash
|
161
|
+
end
|
162
|
+
|
163
|
+
self.class.response_filters.each do |filter|
|
164
|
+
response_hash = filter.call(response_hash)
|
165
|
+
end
|
166
|
+
|
167
|
+
self.class.response_formatters.each do |filter|
|
168
|
+
processed_result = filter.call(response_hash, processed_result)
|
169
|
+
end
|
170
|
+
|
171
|
+
processed_result
|
172
|
+
end
|
173
|
+
|
174
|
+
protected
|
175
|
+
|
176
|
+
# @return [Hash] Apidiesel configuration options
|
177
|
+
def config
|
178
|
+
Apidiesel::CONFIG[environment]
|
179
|
+
end
|
180
|
+
|
181
|
+
def logger
|
182
|
+
@api.logger
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
module Apidiesel
|
2
|
+
|
3
|
+
# This is the abstract main interface class for the Apidiesel gem. It is meant to be
|
4
|
+
# inherited from:
|
5
|
+
#
|
6
|
+
# module MyApi
|
7
|
+
# class Api < Apidiesel::Api
|
8
|
+
# end
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# Apidiesel expects there to be an `Actions` namespace alongside the same scope,
|
12
|
+
# in which it can find the individual endpoint definitions for this API:
|
13
|
+
#
|
14
|
+
# module MyApi
|
15
|
+
# class Api < Apidiesel::Api
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# module Actions
|
19
|
+
# class Action1; end
|
20
|
+
# class Action2; end
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# # Registers endpoints Action1 and Action2
|
25
|
+
# MyApi::Api.register_actions
|
26
|
+
#
|
27
|
+
class Api
|
28
|
+
class << self
|
29
|
+
def request_handlers
|
30
|
+
@request_handlers ||= []
|
31
|
+
end
|
32
|
+
|
33
|
+
def response_handlers
|
34
|
+
@response_handlers ||= []
|
35
|
+
end
|
36
|
+
|
37
|
+
def exception_handlers
|
38
|
+
@exception_handlers ||= []
|
39
|
+
end
|
40
|
+
|
41
|
+
def config(key = nil, value = nil)
|
42
|
+
@config ||= {}
|
43
|
+
|
44
|
+
if key && value
|
45
|
+
@config[key] = value
|
46
|
+
else
|
47
|
+
@config
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Combined getter/setter for this actions URL
|
52
|
+
#
|
53
|
+
# Falls back to the Api setting if blank.
|
54
|
+
#
|
55
|
+
# @param [String] value
|
56
|
+
def url(base_url = nil)
|
57
|
+
if base_url
|
58
|
+
config[:url] = base_url
|
59
|
+
else
|
60
|
+
config[:url]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Combined getter/setter for the HTTP method used
|
65
|
+
#
|
66
|
+
# Falls back to the Api setting if blank.
|
67
|
+
#
|
68
|
+
# @param [String] value
|
69
|
+
def http_method(value = nil)
|
70
|
+
if value
|
71
|
+
config[:http_method] = value
|
72
|
+
else
|
73
|
+
config[:http_method]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Registers a handler for requests and/or responses
|
78
|
+
#
|
79
|
+
# @param [Class] klass
|
80
|
+
|
81
|
+
def use(klass, *args, &block)
|
82
|
+
request_handler = "#{klass.name}::RequestHandler".safe_constantize
|
83
|
+
response_handler = "#{klass.name}::ResponseHandler".safe_constantize
|
84
|
+
exception_handler = "#{klass.name}::ExceptionHandler".safe_constantize
|
85
|
+
|
86
|
+
request_handlers << request_handler.new(*args, &block) if request_handler
|
87
|
+
response_handlers << response_handler.new(*args, &block) if response_handler
|
88
|
+
exception_handlers << exception_handler.new(*args, &block) if exception_handler
|
89
|
+
end
|
90
|
+
|
91
|
+
# Registers the individual API endpoint definitions
|
92
|
+
def register_actions
|
93
|
+
namespace = "#{self.name.deconstantize}::Actions".safe_constantize
|
94
|
+
|
95
|
+
namespace.constants.each do |action|
|
96
|
+
namespace.const_get(action).register(self)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def logger(logger = nil)
|
101
|
+
if logger
|
102
|
+
@logger = logger
|
103
|
+
else
|
104
|
+
@logger
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# @param [Hash] *args
|
110
|
+
def initialize(*args)
|
111
|
+
@config = args.extract_options!.reverse_merge(self.class.config)
|
112
|
+
end
|
113
|
+
|
114
|
+
def logger
|
115
|
+
self.class.logger
|
116
|
+
end
|
117
|
+
|
118
|
+
protected
|
119
|
+
|
120
|
+
def execute_request(action_klass, *args)
|
121
|
+
request = action_klass.new(self).build_request(*args)
|
122
|
+
|
123
|
+
self.class.request_handlers.each do |handler|
|
124
|
+
request = handler.run(request, @config)
|
125
|
+
break if request.response_body.present?
|
126
|
+
end
|
127
|
+
|
128
|
+
unless request.response_body.present?
|
129
|
+
raise "All request handlers failed to deliver a response"
|
130
|
+
end
|
131
|
+
|
132
|
+
self.class.response_handlers.each do |handler|
|
133
|
+
request = handler.run(request, @config)
|
134
|
+
end
|
135
|
+
|
136
|
+
response_handler_klasses =
|
137
|
+
self.class.response_handlers.collect { |handler| handler.class.name.to_s.demodulize }
|
138
|
+
|
139
|
+
# Execute the actions' `responds_with` block automatically, unless
|
140
|
+
# the handler has been included manually in order to control the
|
141
|
+
# order in which the handlers are run
|
142
|
+
unless response_handler_klasses.include?('ActionResponseProcessor')
|
143
|
+
request.process_response
|
144
|
+
end
|
145
|
+
|
146
|
+
request
|
147
|
+
rescue => e
|
148
|
+
self.class.exception_handlers.each do |handler|
|
149
|
+
request = handler.run(e, request, @config)
|
150
|
+
end
|
151
|
+
|
152
|
+
raise e
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,286 @@
|
|
1
|
+
module Apidiesel
|
2
|
+
module Dsl
|
3
|
+
# Defines the input parameters expected for this API action.
|
4
|
+
#
|
5
|
+
# @example
|
6
|
+
# expects do
|
7
|
+
# string :query
|
8
|
+
# integer :per_page, :optional => true, :default => 10
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# See the {Apidiesel::Dsl::ExpectationBuilder ExpectationBuilder} instance methods
|
12
|
+
# for more information on what to use within `expect`.
|
13
|
+
#
|
14
|
+
# @macro [attach] expects
|
15
|
+
# @yield [Apidiesel::Dsl::ExpectationBuilder]
|
16
|
+
def expects(&block)
|
17
|
+
builder = ExpectationBuilder.new
|
18
|
+
builder.instance_eval(&block)
|
19
|
+
parameter_validations.concat builder.parameter_validations
|
20
|
+
end
|
21
|
+
|
22
|
+
# Defines the expected content and format of the response for this API action.
|
23
|
+
#
|
24
|
+
# @example
|
25
|
+
# responds_with do
|
26
|
+
# string :user_id
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# See the {Apidiesel::Dsl::FilterBuilder FilterBuilder} instance methods
|
30
|
+
# for more information on what to use within `responds_with`.
|
31
|
+
#
|
32
|
+
# @macro [attach] responds_with
|
33
|
+
# @yield [Apidiesel::Dsl::FilterBuilder]
|
34
|
+
def responds_with(&block)
|
35
|
+
builder = FilterBuilder.new
|
36
|
+
builder.instance_eval(&block)
|
37
|
+
response_filters.concat builder.response_filters
|
38
|
+
response_formatters.concat builder.response_formatters
|
39
|
+
end
|
40
|
+
|
41
|
+
# ExpectationBuilder defines the methods available within an `expects` block
|
42
|
+
# when defining an API action.
|
43
|
+
class ExpectationBuilder
|
44
|
+
attr_accessor :parameter_validations
|
45
|
+
|
46
|
+
def initialize
|
47
|
+
@parameter_validations = []
|
48
|
+
end
|
49
|
+
|
50
|
+
# Defines a string parameter.
|
51
|
+
#
|
52
|
+
# @example
|
53
|
+
# expects do
|
54
|
+
# string :email, :submitted_as => :username
|
55
|
+
# string :value1, :optional_if_present => :value2
|
56
|
+
# string :value2, :optional_if_present => :value1
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# # This action expects to be given an 'email', which is sent to the API as 'username',
|
60
|
+
# # and requires either a 'value1', a 'value2' or both to be present.
|
61
|
+
#
|
62
|
+
# @param [Symbol] param_name name of the parameter
|
63
|
+
# @param [Hash] *args
|
64
|
+
# @option *args [Boolean] :optional (false) defines whether this parameter may be omitted
|
65
|
+
# @option *args [Symbol] :optional_if_present param_name is optional, if the parameter given here is present instead
|
66
|
+
# @option *args [Symbol] :submitted_as submit param_name to the API under the name given here
|
67
|
+
# @option *args [Object] :default a default parameter to be set when no value is specified
|
68
|
+
# @option *args [Enumerable] :allowed_values only accept the values in this Enumerable.
|
69
|
+
# If Enumerable is a Hash, use the hash values to define what is actually
|
70
|
+
# sent to the server. Example: `:allowed_values => {:foo => "f"}` allows
|
71
|
+
# the value ':foo', but sends it as 'f'
|
72
|
+
def string(param_name, *args)
|
73
|
+
validation_builder(:to_s, param_name, *args)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Defines an integer parameter.
|
77
|
+
#
|
78
|
+
# @example
|
79
|
+
# expects do
|
80
|
+
# integer :per_page, :optional => true
|
81
|
+
# end
|
82
|
+
#
|
83
|
+
# @param (see #string)
|
84
|
+
# @option (see #string)
|
85
|
+
def integer(param_name, *args)
|
86
|
+
validation_builder(:to_i, param_name, *args)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Defines a boolean parameter.
|
90
|
+
#
|
91
|
+
# FIXME: sensible duck typing check
|
92
|
+
#
|
93
|
+
# @example
|
94
|
+
# expects do
|
95
|
+
# boolean :per_page, :optional => true
|
96
|
+
# end
|
97
|
+
#
|
98
|
+
# @param (see #string)
|
99
|
+
# @option (see #string)
|
100
|
+
def boolean(param_name, *args)
|
101
|
+
validation_builder(:to_s, param_name, *args)
|
102
|
+
end
|
103
|
+
|
104
|
+
protected
|
105
|
+
|
106
|
+
def validation_builder(duck_typing_check, param_name, *args)
|
107
|
+
options = args.extract_options!
|
108
|
+
|
109
|
+
parameter_validations << lambda do |given_params, processed_params|
|
110
|
+
if options[:default]
|
111
|
+
given_params[param_name] ||= options[:default]
|
112
|
+
end
|
113
|
+
|
114
|
+
if options.has_key?(:optional_if_present)
|
115
|
+
options[:optional] = true unless given_params[ options[:optional_if_present] ].blank?
|
116
|
+
end
|
117
|
+
|
118
|
+
unless options.has_key?(:optional) && options[:optional] == true
|
119
|
+
raise Apidiesel::InputError, "missing arg: #{param_name} - options: #{options.inspect}" unless given_params.has_key?(param_name) && !given_params[param_name].blank?
|
120
|
+
raise Apidiesel::InputError, "invalid arg #{param_name}: must respond to #{duck_typing_check}" unless given_params[param_name].respond_to?(duck_typing_check)
|
121
|
+
end
|
122
|
+
|
123
|
+
if options.has_key?(:allowed_values) && !given_params[param_name].blank?
|
124
|
+
unless options[:allowed_values].include?(given_params[param_name])
|
125
|
+
raise Apidiesel::InputError, "value '#{given_params[param_name]}' is not a valid value for #{param_name}"
|
126
|
+
end
|
127
|
+
|
128
|
+
if options[:allowed_values].is_a? Hash
|
129
|
+
given_params[param_name] = options[:allowed_values][ given_params[param_name] ]
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
if options[:submitted_as]
|
134
|
+
processed_params[ options[:submitted_as] ] = given_params[param_name]
|
135
|
+
else
|
136
|
+
processed_params[param_name] = given_params[param_name]
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# FilterBuilder defines the methods available within an `responds_with` block
|
143
|
+
# when defining an API action.
|
144
|
+
class FilterBuilder
|
145
|
+
attr_accessor :response_filters, :response_formatters
|
146
|
+
|
147
|
+
def initialize
|
148
|
+
@response_filters = []
|
149
|
+
@response_formatters = []
|
150
|
+
end
|
151
|
+
|
152
|
+
# Returns `key` from the API response as a string.
|
153
|
+
#
|
154
|
+
# @param [Symbol] key the key name to be returned as a string
|
155
|
+
# @param [Hash] *args
|
156
|
+
# @option *args [Symbol] :within look up the key in a namespace (nested hash)
|
157
|
+
def string(key, *args)
|
158
|
+
copy_value_directly(key, *args)
|
159
|
+
end
|
160
|
+
|
161
|
+
# Returns `key` from the API response as an integer.
|
162
|
+
#
|
163
|
+
# @param (see #string)
|
164
|
+
# @option (see #string)
|
165
|
+
def integer(key, *args)
|
166
|
+
copy_value_directly(key, *args)
|
167
|
+
end
|
168
|
+
|
169
|
+
# Returns `key` from the API response as a hash.
|
170
|
+
#
|
171
|
+
# @param (see #string)
|
172
|
+
# @option (see #string)
|
173
|
+
def hash(key, *args)
|
174
|
+
copy_value_directly(key, *args)
|
175
|
+
end
|
176
|
+
|
177
|
+
# Returns `key` from the API response as an array.
|
178
|
+
#
|
179
|
+
# @param (see #string)
|
180
|
+
# @option (see #string)
|
181
|
+
def array(key, *args)
|
182
|
+
copy_value_directly(key, *args)
|
183
|
+
end
|
184
|
+
|
185
|
+
# Returns the API response processed or wrapped in wrapper objects.
|
186
|
+
#
|
187
|
+
# @example
|
188
|
+
# responds_with do
|
189
|
+
# object :issues, :processed_with => lambda { |data| data.delete_if { |k,v| k == 'www_id' } }
|
190
|
+
# end
|
191
|
+
#
|
192
|
+
# @example
|
193
|
+
#
|
194
|
+
# responds_with do
|
195
|
+
# object :issues, :wrapped_in => Apidiesel::ResponseObjects::Topic
|
196
|
+
# end
|
197
|
+
#
|
198
|
+
# @param [Symbol] key the key name to be wrapped or processed
|
199
|
+
# @option *args [Symbol] :within look up the key in a namespace (nested hash)
|
200
|
+
# @option *args [Proc] :processed_with yield the data to this Proc for processing
|
201
|
+
# @option *args [Class] :wrapped_in wrapper object, will be called as `Object.create(data)`
|
202
|
+
# @option *args [Symbol] :as key name to save the result as
|
203
|
+
def objects(key, *args)
|
204
|
+
options = args.extract_options!
|
205
|
+
|
206
|
+
response_formatters << lambda do |data, processed_data|
|
207
|
+
d = get_value(key, data, options[:within])
|
208
|
+
|
209
|
+
if options[:processed_with]
|
210
|
+
d = options[:processed_with].call(d)
|
211
|
+
end
|
212
|
+
if options[:wrapped_in]
|
213
|
+
d = options[:wrapped_in].send(:create, d)
|
214
|
+
end
|
215
|
+
|
216
|
+
result_key = options[:as] || key
|
217
|
+
|
218
|
+
processed_data[result_key] = d
|
219
|
+
|
220
|
+
processed_data
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
# Descends into the hash key hierarchy
|
225
|
+
#
|
226
|
+
# Useful for cutting out useless top-level keys
|
227
|
+
#
|
228
|
+
# @param [Symbol, Array] key
|
229
|
+
def set_scope(key)
|
230
|
+
response_filters << lambda do |data|
|
231
|
+
fetch_path(data, *key)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# Raises an Apidiesel::ResponseError if the callable returns true
|
236
|
+
#
|
237
|
+
# @example
|
238
|
+
# responds_with do
|
239
|
+
# response_error_if ->(data) { data[:code] != 0 },
|
240
|
+
# message: ->(data) { data[:message] }
|
241
|
+
#
|
242
|
+
# @param [Lambda, Proc] callable
|
243
|
+
# @param [String, Lambda, Proc] message
|
244
|
+
# @raises [Apidiesel::ResponseError]
|
245
|
+
def response_error_if(callable, message:)
|
246
|
+
response_formatters << lambda do |data, processed_data|
|
247
|
+
return processed_data unless callable.call(data)
|
248
|
+
|
249
|
+
message = message.is_a?(String) ? message : message.call(data)
|
250
|
+
|
251
|
+
raise ResponseError.new(message)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
protected
|
256
|
+
|
257
|
+
def get_value(key, hash, namespace = nil)
|
258
|
+
if namespace.is_a?(Array)
|
259
|
+
fetch_path(hash, *namespace)
|
260
|
+
elsif namespace.nil?
|
261
|
+
hash[key]
|
262
|
+
else
|
263
|
+
hash[namespace][key]
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
def fetch_path(hash, *parts)
|
268
|
+
parts.reduce(hash) do |memo, key|
|
269
|
+
memo[key] if memo
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def copy_value_directly(key, *args)
|
274
|
+
options = args.extract_options!
|
275
|
+
|
276
|
+
response_formatters << lambda do |data, processed_data|
|
277
|
+
processed_data[key] = get_value(key, data, options[:within])
|
278
|
+
|
279
|
+
processed_data
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
end
|
284
|
+
|
285
|
+
end
|
286
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Apidiesel
|
2
|
+
class Error < StandardError; end
|
3
|
+
class InputError < Error; end
|
4
|
+
|
5
|
+
class RequestError < Error
|
6
|
+
attr_reader :request
|
7
|
+
|
8
|
+
def initialize(msg = nil, request = nil)
|
9
|
+
@request = request
|
10
|
+
super(msg)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class ResponseError < RequestError; end
|
15
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Apidiesel
|
2
|
+
module Handlers
|
3
|
+
module HttpRequestHelper
|
4
|
+
protected
|
5
|
+
|
6
|
+
# Executes a HTTP request
|
7
|
+
#
|
8
|
+
# @param [Apidiesel::Request] request
|
9
|
+
# @param [Hash] payload the request body
|
10
|
+
# @param [Hash] api_config the configuration data of the Apidiesel::Api
|
11
|
+
# instance, as given to the handlers #run method
|
12
|
+
#
|
13
|
+
def execute_request(request:, payload:, api_config:)
|
14
|
+
http_request = HTTPI::Request.new(request.action.url)
|
15
|
+
http_request.body = payload
|
16
|
+
|
17
|
+
http_request.auth.ssl.verify_mode = api_config[:ssl_verify_mode] || :peer
|
18
|
+
http_request.open_timeout = api_config[:timeout] || 30
|
19
|
+
http_request.read_timeout = api_config[:timeout] || 30
|
20
|
+
|
21
|
+
request.http_request = http_request
|
22
|
+
|
23
|
+
begin
|
24
|
+
response = HTTPI.request(request.action.http_method, http_request)
|
25
|
+
request.http_response = response
|
26
|
+
rescue => e
|
27
|
+
raise RequestError.new(e, request)
|
28
|
+
end
|
29
|
+
|
30
|
+
if response.error?
|
31
|
+
raise RequestError.new("#{request.action.http_method} #{request.action.url} returned #{response.code}", request)
|
32
|
+
end
|
33
|
+
|
34
|
+
request
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Apidiesel
|
2
|
+
module Handlers
|
3
|
+
module MockResponse
|
4
|
+
class RequestHandler
|
5
|
+
def run(request, api_config)
|
6
|
+
action = request.action
|
7
|
+
|
8
|
+
return request unless action.respond_to?(:mock_response) && action.mock_response
|
9
|
+
|
10
|
+
file_name = action.mock_response[:file]
|
11
|
+
parser = action.mock_response[:parser]
|
12
|
+
file = File.read(file_name)
|
13
|
+
|
14
|
+
request.response_body = if parser
|
15
|
+
parser.call(file)
|
16
|
+
|
17
|
+
elsif file_name.ends_with?('.json')
|
18
|
+
JSON.parse(file)
|
19
|
+
|
20
|
+
elsif file_name.ends_with?('.xml')
|
21
|
+
Hash.from_xml(file)
|
22
|
+
|
23
|
+
else
|
24
|
+
file
|
25
|
+
end
|
26
|
+
|
27
|
+
request
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module ActionExtension
|
32
|
+
extend ActiveSupport::Concern
|
33
|
+
|
34
|
+
class_methods do
|
35
|
+
def mock_response!(file:, &block)
|
36
|
+
@mock_response = {
|
37
|
+
file: file,
|
38
|
+
parser: block
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
def mock_response
|
43
|
+
@mock_response
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def mock_response
|
48
|
+
self.class.mock_response
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
Apidiesel::Action.send(:include, Apidiesel::Handlers::MockResponse::ActionExtension)
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Apidiesel
|
2
|
+
|
3
|
+
# Wrapper for API requests
|
4
|
+
class Request
|
5
|
+
attr_accessor :action, :parameters, :response_body, :http_request, :http_response, :metadata, :result
|
6
|
+
|
7
|
+
# @param [Apidiesel::Action] action
|
8
|
+
# @param [Hash] parameters
|
9
|
+
# @param [Hash] metadata
|
10
|
+
def initialize(action:, parameters:, metadata: {})
|
11
|
+
@action = action
|
12
|
+
@parameters = parameters
|
13
|
+
@metadata = metadata
|
14
|
+
end
|
15
|
+
|
16
|
+
def response_body
|
17
|
+
@response_body || http_response.try(:body)
|
18
|
+
end
|
19
|
+
|
20
|
+
def process_response
|
21
|
+
# Reraise ResponseErrors to include ourselves. Not
|
22
|
+
# pretty, but I can't think of anything nicer right now
|
23
|
+
begin
|
24
|
+
@result = action.process_response(response_body)
|
25
|
+
rescue ResponseError => e
|
26
|
+
raise ResponseError.new(e.message, self)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_s
|
31
|
+
[
|
32
|
+
"Apidiesel::Request",
|
33
|
+
action.http_method.to_s.upcase,
|
34
|
+
action.url,
|
35
|
+
action.endpoint,
|
36
|
+
parameters.collect { |key, value| "#{key}: #{value}"}.join(',')
|
37
|
+
].join(' ')
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/apidiesel.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'httpi'
|
2
|
+
|
3
|
+
require 'apidiesel/version'
|
4
|
+
|
5
|
+
require 'apidiesel/errors'
|
6
|
+
require 'apidiesel/api'
|
7
|
+
require 'apidiesel/dsl'
|
8
|
+
require 'apidiesel/request'
|
9
|
+
require 'apidiesel/action'
|
10
|
+
require 'apidiesel/handlers/action_response_processor'
|
11
|
+
require 'apidiesel/handlers/http_request_helper'
|
metadata
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: apidiesel
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jan-Christian Foeh
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-11-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 4.2.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 4.2.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: httpi
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 2.4.1
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 2.4.1
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.10'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.10'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '10.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '10.0'
|
69
|
+
description:
|
70
|
+
email:
|
71
|
+
- jan@programmanstalt.de
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- ".gitignore"
|
77
|
+
- ".ruby-gemset"
|
78
|
+
- ".ruby-version"
|
79
|
+
- Gemfile
|
80
|
+
- LICENSE
|
81
|
+
- README.md
|
82
|
+
- Rakefile
|
83
|
+
- apidiesel.gemspec
|
84
|
+
- bin/console
|
85
|
+
- bin/setup
|
86
|
+
- lib/apidiesel.rb
|
87
|
+
- lib/apidiesel/action.rb
|
88
|
+
- lib/apidiesel/api.rb
|
89
|
+
- lib/apidiesel/dsl.rb
|
90
|
+
- lib/apidiesel/errors.rb
|
91
|
+
- lib/apidiesel/handlers/action_response_processor.rb
|
92
|
+
- lib/apidiesel/handlers/http_request_helper.rb
|
93
|
+
- lib/apidiesel/handlers/mock_response.rb
|
94
|
+
- lib/apidiesel/request.rb
|
95
|
+
- lib/apidiesel/version.rb
|
96
|
+
homepage: https://github.com/janfoeh/apidiesel
|
97
|
+
licenses:
|
98
|
+
- MIT
|
99
|
+
metadata: {}
|
100
|
+
post_install_message:
|
101
|
+
rdoc_options: []
|
102
|
+
require_paths:
|
103
|
+
- lib
|
104
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - ">="
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '0'
|
109
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: '0'
|
114
|
+
requirements: []
|
115
|
+
rubyforge_project:
|
116
|
+
rubygems_version: 2.4.5.1
|
117
|
+
signing_key:
|
118
|
+
specification_version: 4
|
119
|
+
summary: Build API clients through an expressive DSL
|
120
|
+
test_files: []
|