api-transformer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,62 @@
1
+ require "json"
2
+ require "hashie"
3
+
4
+ module ApiTransformer
5
+ # Container for backend response data
6
+ class BackendResponse
7
+ # Subclasses Hash so that it can interchangeably use string or symbol access
8
+ class Data < Hash
9
+ include Hashie::Extensions::IndifferentAccess
10
+ end
11
+
12
+ attr_reader :body
13
+
14
+ def initialize(http)
15
+ @http = http
16
+ @body = ""
17
+
18
+ @http.stream { |chunk| @body += chunk }
19
+ end
20
+
21
+ def success?
22
+ status >= 200 && status < 300
23
+ end
24
+
25
+ def status
26
+ @http.response_header.http_status
27
+ end
28
+
29
+ def json
30
+ @data ||= Data.try_convert(JSON.parse(@body))
31
+ rescue JSON::ParserError
32
+ {}
33
+ end
34
+
35
+ def [](key)
36
+ json[key]
37
+ end
38
+
39
+ def cookies
40
+ pairs = @http.response_header["SET_COOKIE"].split("\s*;\s*")
41
+ data = pairs.reduce({}) do |hash, pair|
42
+ key, value = pair.split("=")
43
+ hash.merge(key => value)
44
+ end
45
+
46
+ Data.try_convert(data)
47
+ end
48
+
49
+ def stream(&block)
50
+ if block
51
+ block.call(@body)
52
+
53
+ @http.stream(&block)
54
+ EM::Synchrony::Iterator.new([block]).each do |_, iter|
55
+ @http.callback { iter.next }
56
+ end
57
+ else
58
+ fail "a block is required when streaming"
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,120 @@
1
+ module ApiTransformer
2
+ # Processes endpoint blocks
3
+ class Endpoint
4
+ def initialize(base_url, env, route)
5
+ @base_url = base_url
6
+ @env = env
7
+ @route = route
8
+
9
+ @backend_request_senders = []
10
+ @backend_responses = {}
11
+ end
12
+
13
+ def run
14
+ fail "Endpoints must define a response block" unless @builder
15
+
16
+ send_requests
17
+ send_response
18
+ end
19
+
20
+ def complete
21
+ if @error
22
+ complete_error
23
+ else
24
+ complete_success
25
+ end
26
+ end
27
+
28
+ def request(name, options = {}, &block)
29
+ return unless !options.key?(:when) || options[:when]
30
+
31
+ @backend_request_senders << BackendRequestSender.new(
32
+ name,
33
+ options.merge(base_url: @base_url),
34
+ block,
35
+ @env["client-headers"],
36
+ @route[:helper_blocks]
37
+ )
38
+ end
39
+
40
+ def response(options = {}, &block)
41
+ @builder = FrontendResponseBuilder.new(
42
+ @env,
43
+ options,
44
+ block,
45
+ @route
46
+ )
47
+ end
48
+
49
+ def stream(value)
50
+ @stream = value
51
+ end
52
+
53
+ private
54
+
55
+ def send_requests
56
+ @multi = EventMachine::MultiRequest.new
57
+ iterator = EM::Synchrony::Iterator.new(@backend_request_senders)
58
+
59
+ iterator.each do |sender, iter|
60
+ send_request(sender, iter)
61
+ end
62
+ end
63
+
64
+ def send_request(sender, iter)
65
+ http = sender.send(@backend_responses)
66
+ @backend_responses[sender.request_name] = BackendResponse.new(http)
67
+ @multi.add sender.request_name, http
68
+ http.headers { iter.next }
69
+ rescue ApiTransformer::RequestError => e
70
+ @error = e
71
+ iter.next
72
+ end
73
+
74
+ def send_response
75
+ if @stream
76
+ status_and_headers
77
+ else
78
+ if @backend_request_senders.any? && !@error
79
+ EM::Synchrony::Iterator.new([@multi]).each do |multi, iter|
80
+ multi.callback { iter.next }
81
+ end
82
+ end
83
+
84
+ status_and_headers
85
+ end
86
+ end
87
+
88
+ def status_and_headers
89
+ if @error
90
+ [400, {}]
91
+ else
92
+ @builder.status_and_headers(@backend_responses)
93
+ end
94
+ end
95
+
96
+ def complete_success
97
+ EM.synchrony do
98
+ @builder.do_streaming
99
+
100
+ if @backend_request_senders.any?
101
+ @multi.callback do
102
+ close
103
+ end
104
+ else
105
+ close
106
+ end
107
+ end
108
+ end
109
+
110
+ def complete_error
111
+ @builder.stream_write @error
112
+ close
113
+ end
114
+
115
+ def close
116
+ @builder.send_body
117
+ @builder.stream_close
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,4 @@
1
+ module ApiTransformer
2
+ # Error with the frontend request coming in
3
+ class RequestError < StandardError; end
4
+ end
@@ -0,0 +1,52 @@
1
+ require "json"
2
+
3
+ module ApiTransformer
4
+ # Container for frontend response data
5
+ class FrontendResponse
6
+ attr_accessor :status
7
+ attr_writer :body, :content_type
8
+
9
+ def set(key, value)
10
+ @hash ||= {}
11
+ @hash[key] = value
12
+ end
13
+
14
+ def set_cookie(key, value)
15
+ @cookies ||= {}
16
+ @cookies[key] = value
17
+ end
18
+
19
+ def body
20
+ @hash && @hash.to_json || @body
21
+ end
22
+
23
+ def headers
24
+ cookie_header.merge(content_type_header)
25
+ end
26
+
27
+ private
28
+
29
+ def cookie_header
30
+ if @cookies
31
+ pairs = @cookies.map { |key, value| "#{key}=#{value}" }
32
+ { "Set-Cookie" => pairs.join("; ") }
33
+ else
34
+ {}
35
+ end
36
+ end
37
+
38
+ def content_type_header
39
+ { "Content-Type" => content_type }
40
+ end
41
+
42
+ def content_type
43
+ if @content_type
44
+ @content_type
45
+ elsif @hash
46
+ "application/json"
47
+ else
48
+ "text/plain"
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,138 @@
1
+ module ApiTransformer
2
+ # Processes the response block
3
+ class FrontendResponseBuilder
4
+ attr_reader :success
5
+
6
+ def initialize(env, options, block, route)
7
+ @env = env
8
+ @options = options
9
+ @block = block
10
+ @route = route
11
+
12
+ @frontend_response = FrontendResponse.new
13
+ end
14
+
15
+ def status_and_headers(backend_responses)
16
+ @route[:helper_blocks].each { |block| instance_eval(&block) }
17
+ instance_exec(backend_responses, &@block)
18
+ add_failure_handlers(backend_responses)
19
+ handle_success_or_failure(backend_responses)
20
+
21
+ [@frontend_response.status, @frontend_response.headers]
22
+ end
23
+
24
+ def body(value)
25
+ @frontend_response.body = value
26
+ end
27
+
28
+ def streaming?
29
+ options[:streaming]
30
+ end
31
+
32
+ def success(value = nil, &block)
33
+ @frontend_response.status = value
34
+ @success = block
35
+ end
36
+
37
+ def failure(run = true, &block)
38
+ run && !@failure && @failure = block
39
+ end
40
+
41
+ def status(value)
42
+ @frontend_response.status = value
43
+ end
44
+
45
+ def content_type(value)
46
+ @frontend_response.content_type = value
47
+ end
48
+
49
+ def cookie(key, value)
50
+ @frontend_response.set_cookie(key, value)
51
+ end
52
+
53
+ def attribute(key, value)
54
+ @frontend_response.set key, value
55
+ end
56
+
57
+ def object(key, klass, object_data)
58
+ @frontend_response.set key, object_hash(klass, object_data)
59
+ end
60
+
61
+ def array(key, klass, array_data)
62
+ value = array_data.map { |data| object_hash(klass, data) }
63
+ @frontend_response.set key, value
64
+ end
65
+
66
+ def send_body
67
+ stream_write @frontend_response.body
68
+ end
69
+
70
+ def stream(&block)
71
+ if block
72
+ @streaming_block = block
73
+ else
74
+ fail "a block is required when streaming"
75
+ end
76
+ end
77
+
78
+ def do_streaming
79
+ @streaming_block.call if @streaming_block
80
+ end
81
+
82
+ def stream_write(data)
83
+ @env.stream_send data
84
+ end
85
+
86
+ def stream_close
87
+ @env.stream_close
88
+ end
89
+
90
+ private
91
+
92
+ def add_failure_handlers(backend_responses)
93
+ @route[:failure_handlers].each do |block|
94
+ backend_responses.values.each do |backend_response|
95
+ instance_exec(backend_response, &block)
96
+ end
97
+ end
98
+ end
99
+
100
+ def handle_success_or_failure(backend_responses)
101
+ if @failure
102
+ handle_failure
103
+ elsif backend_responses.all? { |_, resp| resp.success? }
104
+ handle_success
105
+ else
106
+ unhandled_failure(backend_responses)
107
+ end
108
+ end
109
+
110
+ def handle_failure
111
+ set_failure_defaults
112
+ instance_eval(&@failure) if @failure
113
+ end
114
+
115
+ def handle_success
116
+ set_success_defaults
117
+ instance_eval(&@success) if @success
118
+ end
119
+
120
+ def unhandled_failure(backend_responses)
121
+ first_failure = backend_responses.values.first { |resp| !resp.success }
122
+ status first_failure.status
123
+ @frontend_response.body = first_failure.body
124
+ end
125
+
126
+ def object_hash(klass, data)
127
+ klass.new(data).to_hash
128
+ end
129
+
130
+ def set_success_defaults
131
+ @frontend_response.status ||= 200
132
+ end
133
+
134
+ def set_failure_defaults
135
+ @frontend_response.status ||= 400
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,8 @@
1
+ require "hashie"
2
+
3
+ module ApiTransformer
4
+ # Request parameters
5
+ class Params < Hash
6
+ include Hashie::Extensions::IndifferentAccess
7
+ end
8
+ end
@@ -0,0 +1,38 @@
1
+ module ApiTransformer
2
+ module Rack
3
+ # Parses out cookies and make them available as request parameters
4
+ class CookieParams
5
+ # Cookie string parser
6
+ module Parser
7
+ def cookie_params(env)
8
+ cookie_string = env["HTTP_COOKIE"]
9
+
10
+ if cookie_string
11
+ cookie_pairs = cookie_string.split(/\s*;\s*/)
12
+
13
+ cookie_pairs.reduce({}) do |hash, pair|
14
+ key, value = pair.split("=")
15
+ hash.merge(key => value)
16
+ end
17
+ else
18
+ {}
19
+ end
20
+ end
21
+ end
22
+
23
+ include Goliath::Rack::Validator
24
+ include Parser
25
+
26
+ def initialize(app)
27
+ @app = app
28
+ end
29
+
30
+ def call(env)
31
+ Goliath::Rack::Validator.safely(env) do
32
+ env["params"].merge!(cookie_params(env))
33
+ @app.call(env)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,71 @@
1
+ require "ostruct"
2
+
3
+ module ApiTransformer
4
+ # Collection of routes
5
+ class Routes
6
+ # A route
7
+ class Route < OpenStruct; end
8
+
9
+ def initialize
10
+ @routes = {}
11
+ end
12
+
13
+ def add(args)
14
+ pattern, path_params = parse_path_definition(args[:path])
15
+
16
+ route = Route.new(
17
+ path_params: path_params,
18
+ options: args[:options],
19
+ block: args[:block],
20
+ failure_handlers: args[:failure_handlers],
21
+ helper_blocks: args[:helper_blocks]
22
+ )
23
+
24
+ store_route(args[:method], pattern, route)
25
+ end
26
+
27
+ def find(raw_method, path)
28
+ method = raw_method.downcase.to_sym
29
+ return unless @routes[method]
30
+
31
+ pattern, route = @routes[method].find do |pattern, _|
32
+ path.match(pattern)
33
+ end
34
+
35
+ [route, path_params(path, route, pattern)]
36
+ end
37
+
38
+ private
39
+
40
+ def parse_path_definition(path)
41
+ parts = path.split(/\//)
42
+
43
+ pattern_string = parts
44
+ .map { |part| part.match(/^\:/) ? "(.*?)" : part }
45
+ .join("\/")
46
+ .tap { |inner| "^#{inner}$" }
47
+
48
+ pattern = Regexp.new("^#{pattern_string}$")
49
+
50
+ params = parts
51
+ .select { |part| part.match(/^\:/) }
52
+ .map { |part| part.gsub(/^\:/, "").to_sym }
53
+
54
+ [pattern, params]
55
+ end
56
+
57
+ def path_params(path, route, pattern)
58
+ if pattern
59
+ matches = path.match(pattern)[1..-1]
60
+ Hash[route[:path_params].zip(matches)]
61
+ else
62
+ {}
63
+ end
64
+ end
65
+
66
+ def store_route(method, pattern, route)
67
+ @routes[method] ||= {}
68
+ @routes[method][pattern] = route
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,4 @@
1
+ # Top-level namespace
2
+ module ApiTransformer
3
+ VERSION = "0.1.0"
4
+ end
@@ -0,0 +1,147 @@
1
+ require "goliath"
2
+
3
+ require_relative "./api_transformer/endpoint"
4
+ require_relative "./api_transformer/errors"
5
+ require_relative "./api_transformer/backend_request"
6
+ require_relative "./api_transformer/backend_request_sender"
7
+ require_relative "./api_transformer/backend_response"
8
+ require_relative "./api_transformer/frontend_response"
9
+ require_relative "./api_transformer/frontend_response_builder"
10
+ require_relative "./api_transformer/params"
11
+ require_relative "./api_transformer/routes"
12
+ require_relative "./api_transformer/rack/cookie_params"
13
+
14
+ module ApiTransformer
15
+ # Inherit from this class to implement an API transformation server:
16
+ #
17
+ # class ExampleServer < ApiTransformer::Server
18
+ # base_url "http://ip.jsontest.com/"
19
+ #
20
+ # get "/ip" do |params|
21
+ # request :ip do
22
+ # path "/"
23
+ # method :get
24
+ # end
25
+ #
26
+ # response do |data|
27
+ # success do
28
+ # status 200
29
+ # attribute :your_ip, data[:ip][:ip]
30
+ # end
31
+ # end
32
+ # end
33
+ # end
34
+ #
35
+ class Server < Goliath::API
36
+ use Goliath::Rack::Params
37
+ use ApiTransformer::Rack::CookieParams
38
+
39
+ def on_headers(env, headers)
40
+ env["client-headers"] = headers
41
+ end
42
+
43
+ def response(env)
44
+ path = env[Goliath::Request::REQUEST_PATH]
45
+ route, path_params = @@routes.find(env["REQUEST_METHOD"], path)
46
+
47
+ if route
48
+ run_route(route, path_params, env)
49
+ else
50
+ [404, {}, "nope"]
51
+ end
52
+ end
53
+
54
+ def run_route(route, path_params, env)
55
+ indifferent_params = Params.try_convert(params.merge(path_params))
56
+ endpoint = Endpoint.new(@@base_url, env, route)
57
+ headers = env["client-headers"]
58
+
59
+ route[:helper_blocks].each { |block| endpoint.instance_eval(&block) }
60
+ endpoint.instance_exec(indifferent_params, headers, &route[:block])
61
+
62
+ status, headers = endpoint.run
63
+
64
+ EM.next_tick do
65
+ endpoint.complete
66
+ end
67
+
68
+ streaming_response(status, headers)
69
+ end
70
+
71
+ class << self
72
+ def inherited(klass)
73
+ klass.use Goliath::Rack::Params
74
+ end
75
+
76
+ def base_url(url)
77
+ @@base_url = url
78
+ end
79
+
80
+ def namespace(path)
81
+ prior = [@namespace, failure_handlers.dup, helper_blocks.dup]
82
+ @namespace = @namespace.to_s + path
83
+
84
+ yield
85
+
86
+ @namespace, @failure_handlers, @helper_blocks = prior
87
+ end
88
+
89
+ def unhandled_failures(&block)
90
+ failure_handlers.unshift(block)
91
+ end
92
+
93
+ def helpers(&block)
94
+ helper_blocks.unshift(block)
95
+ end
96
+
97
+ def get(path = "", options = {}, &block)
98
+ add_route(:get, path, options, block)
99
+ end
100
+
101
+ def post(path = "", options = {}, &block)
102
+ add_route(:post, path, options, block)
103
+ end
104
+
105
+ def put(path = "", options = {}, &block)
106
+ add_route(:put, path, options, block)
107
+ end
108
+
109
+ def delete(path = "", options = {}, &block)
110
+ add_route(:delete, path, options, block)
111
+ end
112
+
113
+ def reset_routes
114
+ @@routes = Routes.new
115
+ end
116
+
117
+ private
118
+
119
+ def add_route(method, path, options, block)
120
+ full_path = "#{@namespace}#{path}"
121
+
122
+ @@routes ||= Routes.new
123
+ @@routes.add(method: method, path: full_path, options: options,
124
+ block: block, failure_handlers: failure_handlers,
125
+ helper_blocks: helper_blocks)
126
+ end
127
+
128
+ def failure_handlers
129
+ @failure_handlers ||= []
130
+ end
131
+
132
+ def helper_blocks
133
+ @helper_blocks ||= []
134
+ end
135
+
136
+ def server
137
+ superclass
138
+ end
139
+ end
140
+
141
+ private
142
+
143
+ def server
144
+ self.class
145
+ end
146
+ end
147
+ end