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 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: []