api-transformer 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: 87a3b324b1f0fd77ef0091e47311ef9e0029b92a
4
+ data.tar.gz: 7cf42588afe1e037df1ce743e4ac2c0e4c5a2398
5
+ SHA512:
6
+ metadata.gz: ddf7f252707a1340651a815251a1597a461495cee7be30a70521c01faea1d5c1394af59c753cb34422e96a3a91d61494fd22f515ed036cd27416fa123ac622a0
7
+ data.tar.gz: e670c237da02b04c635ad9b7ad76acec3612b8640afe0174257903802276d56d55852c5461324885634a41a8d5866b44190a35698f532a2f0fb89f68b1da5fcb
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/.overcommit.yml ADDED
@@ -0,0 +1,27 @@
1
+ # Use this file to configure the Overcommit hooks you wish to use. This will
2
+ # extend the default configuration defined in:
3
+ # https://github.com/causes/overcommit/blob/master/config/default.yml
4
+ #
5
+ # At the topmost level of this YAML file is a key representing type of hook
6
+ # being run (e.g. pre-commit, commit-msg, etc.). Within each type you can
7
+ # customize each hook, such as whether to only run it on certain files (via
8
+ # `include`), whether to only display output if it fails (via `quiet`), etc.
9
+ #
10
+ # For a complete list of hooks, see:
11
+ # https://github.com/causes/overcommit/tree/master/lib/overcommit/hook
12
+ #
13
+ # For a complete list of options that you can use to customize hooks, see:
14
+ # https://github.com/causes/overcommit#configuration
15
+ #
16
+ # Uncomment the following lines to make the configuration take effect.
17
+
18
+ PreCommit:
19
+ Rubocop:
20
+ on_warn: fail # Treat all warnings as failures
21
+
22
+ #PostCheckout:
23
+ # ALL: # Special hook name that customizes all hooks of this type
24
+ # quiet: true # Change all post-checkout hooks to only display output on failure
25
+ #
26
+ # IndexTags:
27
+ # enabled: true # Generate a tags file with `ctags` each time HEAD changes
data/.rubocop.yml ADDED
@@ -0,0 +1,9 @@
1
+ inherit_from: .rubocop_todo.yml
2
+
3
+ # Rubocop defaults to single quotes, but arbitrarily so
4
+ StringLiterals:
5
+ EnforcedStyle: double_quotes
6
+
7
+ # api-tranformer is a DSL
8
+ Style/TrivialAccessors:
9
+ AllowDSLWriters: true
data/.rubocop_todo.yml ADDED
@@ -0,0 +1,15 @@
1
+ # This configuration was generated by `rubocop --auto-gen-config`
2
+ # on 2014-09-12 14:27:20 -0700 using RuboCop version 0.24.1.
3
+ # The point is for the user to remove these configuration records
4
+ # one by one as the offenses are removed from the code base.
5
+ # Note that changes in the inspected code, or installation of new
6
+ # versions of RuboCop, may require this file to be generated again.
7
+
8
+ # Offense count: 2
9
+ Style/ClassVars:
10
+ Enabled: false
11
+
12
+ # Offense count: 1
13
+ # Configuration parameters: CountComments.
14
+ Metrics/ClassLength:
15
+ Max: 110
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.2
4
+ - 2.0.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in api-transformer.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Bruz Marzolf
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,358 @@
1
+ [![Build Status](https://travis-ci.org/CXInc/api-transformer.svg)](https://travis-ci.org/CXInc/api-transformer)
2
+ [![Code Climate](https://codeclimate.com/github/CXInc/api-transformer/badges/gpa.svg)](https://codeclimate.com/github/CXInc/api-transformer)
3
+
4
+ # api-transformer
5
+
6
+ **NOTE: this is currently a work in progress**
7
+
8
+ A Ruby web server DSL for exposing one or more back-end services through a different API.
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ ```ruby
15
+ gem "api-transformer"
16
+ ```
17
+
18
+ And then execute:
19
+
20
+ ```bash
21
+ $ bundle
22
+ ```
23
+
24
+ Or install it yourself as:
25
+
26
+ ```bash
27
+ $ gem install api-transformer
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ### Quick Start
33
+
34
+ Create a file ip_server.rb:
35
+
36
+ ```ruby
37
+ require 'api_transformer'
38
+
39
+ class IpServer < ApiTransformer::Server
40
+ base_url "http://ip.jsontest.com/"
41
+
42
+ get "/ip" do
43
+ request :ip do
44
+ path "/"
45
+ method :get
46
+ end
47
+
48
+ response do |data|
49
+ success do
50
+ status 200
51
+ attribute :your_ip, data[:ip][:ip]
52
+ end
53
+ end
54
+ end
55
+ end
56
+ ```
57
+
58
+ Run it:
59
+
60
+ ```bash
61
+ $ ruby ip_server.rb -sv
62
+ ```
63
+
64
+ Try it:
65
+
66
+ ```bash
67
+ $ curl -i http://localhost:9000/ip
68
+ HTTP/1.1 200 OK
69
+ Server: Goliath
70
+ Date: Tue, 30 Sep 2014 23:19:34 GMT
71
+
72
+ {"your_ip":"74.125.228.96"}
73
+ ```
74
+
75
+ Let's break this down:
76
+
77
+ ```ruby
78
+ base_url "http://ip.jsontest.com/"
79
+ ```
80
+
81
+ Declare the base URL for back-end requests, which will each provide a path under this.
82
+
83
+ ```ruby
84
+ get "/ip" do |params|
85
+ ```
86
+
87
+ This defines a single endpoint that responds to GET requests at the path '/ip'.
88
+
89
+ request :ip do
90
+ path "/"
91
+ method :get
92
+ end
93
+
94
+ It makes a single GET request to the root path of the base_url (http://ip.jsontest.com/).
95
+
96
+ ```ruby
97
+ response do |data|
98
+ success do
99
+ status 200
100
+ attribute :your_ip, data[:ip][:ip]
101
+ end
102
+ end
103
+ ```
104
+
105
+ Upon completion of the request to ip.jsontest.com with a 2XX response, it responds with a status of 200 and a JSON attribute of your_ip.
106
+
107
+ ### Endpoints without any requests
108
+
109
+ Back-end requests are not required.
110
+
111
+ ```ruby
112
+ get "/ping" do
113
+ response do
114
+ success { attribute :result, "pong" }
115
+ end
116
+ end
117
+ ```
118
+
119
+ ### Accepting parameters
120
+
121
+ Query params:
122
+
123
+ ```ruby
124
+ get "/some_params" do
125
+ request :greeting do
126
+ path "/greeting"
127
+ method :get
128
+
129
+ query_param :greeting, "hello"
130
+ query_param :name, "Bob"
131
+ end
132
+
133
+ response do
134
+ success { attribute :result, "hello Bob" }
135
+ end
136
+ end
137
+ ```
138
+
139
+ Form params:
140
+
141
+ ```ruby
142
+ post "/some_params" do
143
+ request :greeting do
144
+ path "/greeting"
145
+ method :post
146
+
147
+ form_param :greeting, "hello"
148
+ form_param :name, "Bob"
149
+ end
150
+
151
+ response do
152
+ success { attribute :result, "hello Bob" }
153
+ end
154
+ end
155
+ ```
156
+
157
+ JSON params:
158
+
159
+ ```ruby
160
+ post "/some_params" do
161
+ request :greeting do
162
+ path "/greeting"
163
+ method :post
164
+
165
+ json_param :greeting, "hello"
166
+ json_param :name, "Bob"
167
+ end
168
+
169
+ response do
170
+ success { attribute :result, "hello Bob" }
171
+ end
172
+ end
173
+ ```
174
+
175
+ JSON params are accepted as a JSON-encoded request body. In this case what would get sent to the back-end endpoint is {"greeting":"hello","name":"Bob"}.
176
+
177
+ Form and JSON params can't both be used on the same request.
178
+
179
+ ### HTTP verbs
180
+
181
+ GET, POST, PUT and DELETE are provided:
182
+
183
+ ```ruby
184
+ delete "/ping" do
185
+ response do
186
+ success { attribute :result, "I should probably delete something" }
187
+ end
188
+ end
189
+ ```
190
+
191
+ ### Pass-through of headers
192
+
193
+ ```ruby
194
+ get "/user_agent" do |_, headers|
195
+ response do
196
+ success { attribute :user_agent, headers["User-Agent"] }
197
+ end
198
+ end
199
+ ```
200
+
201
+ ### JSON objects and arrays
202
+
203
+ Classes can be used to parse data and return JSON objects:
204
+
205
+ ```ruby
206
+ class Bob
207
+ def initialize(last_name)
208
+ @last_name = last_name
209
+ end
210
+
211
+ def to_hash
212
+ { first_name: "Bob", last_name: @last_name }
213
+ end
214
+ end
215
+
216
+ get "/object" do
217
+ response do
218
+ success { object :bob, Bob, "Saget" }
219
+ end
220
+ end
221
+
222
+ # => {"bob":{"first_name":"Bob","last_name":"Saget"}}
223
+ ```
224
+
225
+ These can also be used to return arrays of data:
226
+
227
+ ```ruby
228
+ get "/array" do
229
+ response do
230
+ success { array :bobs, Bob, %w(Ross Marley) }
231
+ end
232
+ end
233
+
234
+ # => {"bobs":[{"first_name":"Bob","last_name":"Ross"},{"first_name":"Bob","last_name":"Marley"}]}
235
+ ```
236
+
237
+ ### Use request data on subsequent requests
238
+
239
+ This is perhaps easiest to explain with an example:
240
+
241
+ ```ruby
242
+ get "/multi" do
243
+ request :one do
244
+ path "/one"
245
+ method :get
246
+ end
247
+
248
+ request :two do |data|
249
+ path "/two"
250
+ method :get
251
+
252
+ query_param :name, data[:one][:name]
253
+ end
254
+
255
+ response do |data|
256
+ success { attribute :name, data[:two][:result] }
257
+ end
258
+ end
259
+
260
+ get "/one" do
261
+ response do
262
+ success { attribute :name, "Bob" }
263
+ end
264
+ end
265
+
266
+ get "/two" do |params|
267
+ response do
268
+ success { attribute :result, params[:name] }
269
+ end
270
+ end
271
+ ```
272
+
273
+ Notably, on this line:
274
+
275
+ ```ruby
276
+ request :two do |data|
277
+ ```
278
+
279
+ `data` will contain the response data from the first request.
280
+
281
+ ### Conditional requests
282
+
283
+ It can be useful to only send back-end requests under certain conditions:
284
+
285
+ ```ruby
286
+ get "/conditional" do |params|
287
+ request :start_background_job, when: params[:make_it_so] do
288
+ path "/job"
289
+ method :post
290
+ end
291
+
292
+ response do
293
+ success { attribute :result, "OK" }
294
+ end
295
+ end
296
+ ```
297
+
298
+ ### Streaming
299
+
300
+ It can be done without a back-end request:
301
+
302
+ ```ruby
303
+ get "/stream_no_backend" do
304
+ stream true
305
+
306
+ response do
307
+ success do
308
+ stream do
309
+ (1..9).each { |n| stream_write n }
310
+ end
311
+ end
312
+ end
313
+ end
314
+ ```
315
+
316
+ Things to notice:
317
+ - Inside the endpoint block, `stream true` is needed
318
+ - Inside the success block, there's a `stream` block
319
+ - The `stream` block uses `stream_write` to write the response
320
+
321
+ This gets more useful when proxying to a back-end request that is going to return a lot of data:
322
+
323
+ ```ruby
324
+ get "/stream_download" do
325
+ stream true
326
+
327
+ request :download do
328
+ path "/huge_download"
329
+ method :get
330
+ end
331
+
332
+ response do |data|
333
+ success do
334
+ stream do
335
+ data[:one].stream do |chunk|
336
+ stream_write chunk
337
+ end
338
+ end
339
+ end
340
+ end
341
+ end
342
+ ```
343
+
344
+ The `stream do |chunk|` block will receive the data in small chunks as it comes off the socket, so that the entire response body doesn't go into memory.
345
+
346
+ ## Testing
347
+
348
+ ```bash
349
+ $ rake
350
+ ```
351
+
352
+ ## Contributing
353
+
354
+ 1. Fork it ( https://github.com/CXInc/api-transformer/fork )
355
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
356
+ 3. Commit your changes (`git commit -am "Add some feature"`)
357
+ 4. Push to the branch (`git push origin my-new-feature`)
358
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require "rake/testtask"
4
+
5
+ task default: [:test]
6
+
7
+ Rake::TestTask.new do |t|
8
+ t.pattern = "spec/*_spec.rb"
9
+ end
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "api_transformer/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "api-transformer"
8
+ spec.version = ApiTransformer::VERSION
9
+ spec.authors = ["Bruz Marzolf"]
10
+ spec.email = ["bruz@bruzilla.com"]
11
+ spec.summary = "API transformation Ruby DSL and server"
12
+ spec.description = ""
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(/^(test|spec|features)\//)
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "rack", "~> 1.5"
22
+ spec.add_dependency "goliath", "~> 1.0"
23
+ spec.add_dependency "em-http-request", "~> 1.1"
24
+ spec.add_dependency "json", "~> 1.8"
25
+ spec.add_dependency "hashie", "~> 3.3"
26
+
27
+ spec.add_development_dependency "bundler", "~> 1.6"
28
+ spec.add_development_dependency "rake", "~> 10.0"
29
+ spec.add_development_dependency "pry", "~> 0.10"
30
+ spec.add_development_dependency "mocha", "~> 1.1"
31
+ end
@@ -0,0 +1,19 @@
1
+ require_relative '../lib/api_transformer'
2
+
3
+ class IpServer < ApiTransformer::Server
4
+ base_url "http://ip.jsontest.com"
5
+
6
+ get "/ip" do
7
+ request :ip do
8
+ path "/"
9
+ method :get
10
+ end
11
+
12
+ response do |data|
13
+ success do
14
+ status 200
15
+ attribute :your_ip, data[:ip][:ip]
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1 @@
1
+ require_relative "../api_transformer"
@@ -0,0 +1,79 @@
1
+ require "em-synchrony/em-http"
2
+
3
+ module ApiTransformer
4
+ # Process request blocks
5
+ class BackendRequest
6
+ attr_reader :name
7
+ attr_accessor :path, :method, :query_params, :form_params, :json_params,
8
+ :cookie_params
9
+
10
+ def initialize(name, base_url, frontend_headers)
11
+ @name = name
12
+ @base_url = base_url
13
+ @frontend_headers = frontend_headers
14
+
15
+ @query_params = {}
16
+ @form_params = {}
17
+ @json_params = {}
18
+ @cookie_params = {}
19
+ end
20
+
21
+ def send
22
+ url = @base_url + @path
23
+
24
+ EM::HttpRequest.new(url).send(
25
+ "a#{@method}",
26
+ query: @query_params,
27
+ body: body,
28
+ head: headers
29
+ )
30
+ end
31
+
32
+ private
33
+
34
+ def body
35
+ case [@form_params.any?, @json_params.any?]
36
+ when [true, true]
37
+ fail RequestError, "A request cannot have both json and form parameters"
38
+ when [true, false]
39
+ @form_params
40
+ when [false, true]
41
+ @json_params.to_json
42
+ when [false, false]
43
+ nil
44
+ end
45
+ end
46
+
47
+ def headers
48
+ ret = [stripped_frontend_headers, content_type, cookies].reduce(&:merge)
49
+ ret.delete("Host")
50
+ ret
51
+ end
52
+
53
+ def stripped_frontend_headers
54
+ @frontend_headers.reject { |key, _| key == "Content-Length" }
55
+ end
56
+
57
+ def content_type
58
+ if @json_params.any?
59
+ { "Content-Type" => "application/json" }
60
+ elsif @form_params.any?
61
+ { "Content-Type" => "application/x-www-form-urlencoded" }
62
+ else
63
+ {}
64
+ end
65
+ end
66
+
67
+ def cookies
68
+ if @cookie_params.any?
69
+ cookie_string = @cookie_params
70
+ .map { |key, value| "#{key}=#{value};" }
71
+ .join(" ")
72
+
73
+ { "Cookie" => cookie_string }
74
+ else
75
+ {}
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,56 @@
1
+ module ApiTransformer
2
+ # Processes the request block
3
+ class BackendRequestSender
4
+ def initialize(name, options, block, frontend_headers, helper_blocks)
5
+ @backend_request = BackendRequest.new(
6
+ name,
7
+ options[:base_url],
8
+ frontend_headers
9
+ )
10
+
11
+ @block = block
12
+ @helper_blocks = helper_blocks
13
+ end
14
+
15
+ def send(backend_responses)
16
+ @helper_blocks.each { |block| instance_eval(&block) }
17
+ instance_exec(backend_responses, &@block)
18
+
19
+ unless @backend_request.method
20
+ fail RequestError, "Missing method for backend request: #{request_name}"
21
+ end
22
+
23
+ @backend_request.send
24
+ end
25
+
26
+ def request_name
27
+ @backend_request.name
28
+ end
29
+
30
+ private
31
+
32
+ def path(value)
33
+ @backend_request.path = value
34
+ end
35
+
36
+ def method(value)
37
+ @backend_request.method = value
38
+ end
39
+
40
+ def query_param(key, value)
41
+ @backend_request.query_params[key] = value
42
+ end
43
+
44
+ def form_param(key, value)
45
+ @backend_request.form_params[key] = value
46
+ end
47
+
48
+ def json_param(key, value)
49
+ @backend_request.json_params[key] = value
50
+ end
51
+
52
+ def cookie_param(key, value)
53
+ @backend_request.cookie_params[key] = value
54
+ end
55
+ end
56
+ end