apidiesel 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ apidiesel.sublime-project
11
+ apidiesel.sublime-workspace
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ apidiesel
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.2.3
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in apidiesel.gemspec
4
+ gemspec
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,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -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,13 @@
1
+ module Apidiesel
2
+ module Handlers
3
+ module ActionResponseProcessor
4
+ class ResponseHandler
5
+ def run(request, _)
6
+ request.process_response
7
+
8
+ request
9
+ end
10
+ end
11
+ end
12
+ end
13
+ 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
@@ -0,0 +1,3 @@
1
+ module Apidiesel
2
+ VERSION = "0.1.0"
3
+ 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: []