apidiesel 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|