rester 0.0.1

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: 891fe3d740381cb54c48e89ad69fca06e1fc06d6
4
+ data.tar.gz: db402da2918dc425d879b3719fc41e976b1de4bf
5
+ SHA512:
6
+ metadata.gz: 1a4992cf642165b17d1e77e854ac9fdaadef82ccc45337548d0a902048bb372dc9574713ff5109f5baba43e3f0c730b1c299c005932314d1ff5bc803ca6d087b
7
+ data.tar.gz: 4b9ef3355867f57f67632b24dffd84b9d39027d94544d8681058b27e01fe204418dc04b3a9b843ad48fdcf9b412460dda07707bdfbc74ed77bac057b38a347e2
data/lib/rester.rb ADDED
@@ -0,0 +1,23 @@
1
+ require 'rester/version'
2
+ require 'rack'
3
+
4
+ module Rester
5
+ require 'rester/railtie' if defined?(Rails)
6
+ autoload(:Service, 'rester/service')
7
+ autoload(:Errors, 'rester/errors')
8
+ autoload(:Client, 'rester/client')
9
+ autoload(:Utils, 'rester/utils')
10
+ autoload(:Middleware, 'rester/middleware')
11
+
12
+ class << self
13
+ def load_tasks
14
+ Dir[
15
+ File.expand_path("../../tasks", __FILE__) + '/**.rake'
16
+ ].each { |rake_file| load rake_file }
17
+ end
18
+
19
+ def connect(*args)
20
+ Client.new(*args)
21
+ end
22
+ end # Class Methods
23
+ end # Rester
@@ -0,0 +1,61 @@
1
+ require 'json'
2
+
3
+ module Rester
4
+ class Client
5
+ autoload(:Adapters, 'rester/client/adapters')
6
+
7
+ attr_reader :adapter
8
+
9
+ def initialize(*args)
10
+ case args.first
11
+ when Adapters::Adapter
12
+ self.adapter = args.first
13
+ else
14
+ self.adapter = Adapters::HttpAdapter.new(*args)
15
+ end
16
+ end
17
+
18
+ def connect(*args)
19
+ adapter.connect(*args)
20
+ end
21
+
22
+ def connected?
23
+ adapter.connected? && adapter.get(:test_connection).first == 200
24
+ end
25
+
26
+ protected
27
+
28
+ def adapter=(adapter)
29
+ @adapter = adapter
30
+ end
31
+
32
+ private
33
+
34
+ ##
35
+ # Submits the method to the adapter.
36
+ def method_missing(meth, *args, &block)
37
+ verb, meth = Utils.extract_method_verb(meth)
38
+ _process_response(meth, *adapter.request(verb, meth, *args, &block))
39
+ end
40
+
41
+ def _process_response(meth, status, body)
42
+ if status.between?(200, 299)
43
+ _parse_json(body)
44
+ elsif status == 400
45
+ raise Errors::RequestError, _parse_json(body)[:message]
46
+ elsif status == 404
47
+ raise Errors::InvalidMethodError, meth.to_s
48
+ else
49
+ raise Errors::ServerError, _parse_json(body)[:message]
50
+ end
51
+ end
52
+
53
+ def _parse_json(data)
54
+ if data.is_a?(String) && !data.empty?
55
+ JSON.parse(data, symbolize_names: true)
56
+ else
57
+ {}
58
+ end
59
+ end
60
+ end # Client
61
+ end # Rester
@@ -0,0 +1,8 @@
1
+ module Rester
2
+ class Client
3
+ module Adapters
4
+ autoload(:Adapter, 'rester/client/adapters/adapter')
5
+ autoload(:HttpAdapter, 'rester/client/adapters/http_adapter')
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,114 @@
1
+ module Rester
2
+ module Client::Adapters
3
+ class Adapter
4
+ def initialize(*args)
5
+ connect(*args) unless args.empty?
6
+ end
7
+
8
+ ##
9
+ # Returns the headers defined for this Adapter. Optionally, you may also
10
+ # define additional headers you'd like to add/override.
11
+ def headers(new_headers={})
12
+ (@headers ||= {}).merge!(new_headers)
13
+ end
14
+
15
+ ##
16
+ # Connect to a service. The specific arguments depend on the Adapter
17
+ # subclass.
18
+ def connect(*args)
19
+ raise NotImplementedError
20
+ end
21
+
22
+ ##
23
+ # Returns whether or not the Adapter is connected to a service.
24
+ def connected?
25
+ raise NotImplementedError
26
+ end
27
+
28
+ def request(verb, method, *args, &block)
29
+ _validate_verb(verb)
30
+ params = _validate_params(args.pop) if args.last.is_a?(Hash)
31
+ _validate_args(args)
32
+
33
+ public_send(
34
+ "#{verb}!",
35
+ "/#{method}/#{args.map(&:to_s).join('/')}",
36
+ params
37
+ )
38
+ end
39
+
40
+ def get(method, *args, &block)
41
+ request(:get, method, *args, &block)
42
+ end
43
+
44
+ def post(method, *args, &block)
45
+ request(:post, method, *args, &block)
46
+ end
47
+
48
+ def get!(path, params={})
49
+ raise NotImplementedError
50
+ end
51
+
52
+ def post!(path, params={})
53
+ raise NotImplementedError
54
+ end
55
+
56
+ protected
57
+
58
+ def headers=(h)
59
+ @headers = h
60
+ end
61
+
62
+ private
63
+
64
+ VALID_VERBS = {
65
+ get: true,
66
+ post: true
67
+ }.freeze
68
+
69
+ VALID_ARG_TYPES = {
70
+ String => true,
71
+ Symbol => true,
72
+ Fixnum => true,
73
+ Integer => true,
74
+ Float => true
75
+ }.freeze
76
+
77
+ VALID_PARAM_KEY_TYPES = {
78
+ String => true,
79
+ Symbol => true
80
+ }.freeze
81
+
82
+ VALID_PARAM_VALUE_TYPES = {
83
+ String => true,
84
+ Symbol => true,
85
+ Fixnum => true,
86
+ Integer => true,
87
+ Float => true,
88
+ DateTime => true
89
+ }.freeze
90
+
91
+ def _validate_verb(verb)
92
+ VALID_VERBS[verb] or
93
+ raise ArgumentError, "Invalid verb: #{verb.inspect}"
94
+ end
95
+
96
+ def _validate_args(args)
97
+ args.each { |arg|
98
+ VALID_ARG_TYPES[arg.class] or
99
+ raise ArgumentError, "Invalid argument type: #{arg.inspect}"
100
+ }
101
+ end
102
+
103
+ def _validate_params(params)
104
+ params.each { |key, value|
105
+ VALID_PARAM_KEY_TYPES[key.class] or
106
+ raise ArgumentError, "Invalid param key type: #{key.inspect}"
107
+
108
+ VALID_PARAM_VALUE_TYPES[value.class] or
109
+ raise ArgumentError, "Invalid param value type: #{value.inspect}"
110
+ }
111
+ end
112
+ end # Adapter
113
+ end # Client::Adapters
114
+ end # Rester
@@ -0,0 +1,31 @@
1
+ module Rester
2
+ module Client::Adapters
3
+ class HttpAdapter < Adapter
4
+ autoload(:Connection, 'rester/client/adapters/http_adapter/connection')
5
+
6
+ attr_reader :connection
7
+
8
+ def connect(*args)
9
+ nil.tap { @connection = Connection.new(*args) }
10
+ end
11
+
12
+ def connected?
13
+ !!connection
14
+ end
15
+
16
+ def get!(path, params={})
17
+ _prepare_response(connection.get(path, headers: headers, query: params))
18
+ end
19
+
20
+ def post!(path, params={})
21
+ _prepare_response(connection.post(path, headers: headers, data: params))
22
+ end
23
+
24
+ private
25
+
26
+ def _prepare_response(response)
27
+ [response.code.to_i, response.body]
28
+ end
29
+ end # HttpAdapter
30
+ end # Client::Adapters
31
+ end # Rester
@@ -0,0 +1,58 @@
1
+ require 'net/http'
2
+ require 'openssl'
3
+ require 'uri'
4
+
5
+ module Rester
6
+ module Client::Adapters
7
+ class HttpAdapter::Connection
8
+ DEFAULT_POST_HEADERS = {
9
+ "Content-Type".freeze => "application/x-www-form-urlencoded".freeze
10
+ }.freeze
11
+
12
+ attr_reader :url
13
+
14
+ def initialize(url)
15
+ @url = url.is_a?(String) ? URI(url) : url
16
+ @url.path = @url.path[0..-2] if @url.path[-1] == '/'
17
+ end
18
+
19
+ def get(path, params={})
20
+ _http.get(
21
+ _path(path, params[:query]),
22
+ _prepare_headers(params[:headers])
23
+ )
24
+ end
25
+
26
+ def post(path, params={})
27
+ headers = DEFAULT_POST_HEADERS.merge(_prepare_headers(params[:headers]))
28
+ encoded_data = URI.encode_www_form(params[:data] || {})
29
+ _http.post(_path(path), encoded_data, headers)
30
+ end
31
+
32
+ private
33
+
34
+ def _path(path, query=nil)
35
+ u = url.dup
36
+ u.path += path
37
+ u.query = URI.encode_www_form(query) if query && !query.empty?
38
+ u.request_uri
39
+ end
40
+
41
+ def _http
42
+ Net::HTTP.new(url.hostname, url.port).tap { |http|
43
+ if (http.use_ssl=url.is_a?(URI::HTTPS))
44
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
45
+
46
+ http.cert_store = OpenSSL::X509::Store.new.tap { |s|
47
+ s.set_default_paths
48
+ }
49
+ end
50
+ }
51
+ end
52
+
53
+ def _prepare_headers(headers)
54
+ Hash[(headers || {}).map { |k, v| [k.to_s, v.to_s] }]
55
+ end
56
+ end # HttpAdapter::Connection
57
+ end # Client::Adapters
58
+ end # Rester
@@ -0,0 +1,52 @@
1
+ module Rester
2
+ module Errors
3
+ class << self
4
+ ##
5
+ # Throws an error instead of raising it, which is more performant. Must
6
+ # be caught by an appropriate error handling wrapper.
7
+ def throw_error!(klass, message=nil)
8
+ error = message ? klass.new(message) : klass.new
9
+ throw :error, error
10
+ end
11
+ end # Class Methods
12
+
13
+ class Error < StandardError; end
14
+
15
+ ##
16
+ # Packet errors
17
+ class PacketError < Error; end
18
+ class InvalidEncodingError < PacketError; end
19
+
20
+ #############
21
+ # Http Errors
22
+ class HttpError < Error; end
23
+
24
+ ##
25
+ # Request Errors
26
+
27
+ # 400 Error
28
+ class RequestError < HttpError; end
29
+
30
+ # 401 Error
31
+ class AuthenticationError < RequestError; end
32
+
33
+ # 403 Error
34
+ class ForbiddenError < RequestError; end
35
+
36
+ # 404 Not Found
37
+ class NotFoundError < RequestError; end
38
+ class InvalidMethodError < NotFoundError; end
39
+
40
+ # 500 ServerError
41
+ class ServerError < RequestError; end
42
+
43
+ ##
44
+ # Server Errors
45
+
46
+ # General Errors
47
+ class InvalidValueError < Error; end
48
+
49
+ # Rester Errors
50
+ class ServiceNotDefinedError < Error; end
51
+ end # Errors
52
+ end # Rester
@@ -0,0 +1,6 @@
1
+ module Rester
2
+ module Middleware
3
+ autoload(:Base, 'rester/middleware/base')
4
+ autoload(:ErrorHandling, 'rester/middleware/error_handling')
5
+ end
6
+ end
@@ -0,0 +1,44 @@
1
+ module Rester
2
+ module Middleware
3
+ class Base
4
+ attr_reader :app
5
+ attr_reader :options
6
+
7
+ def initialize(app, options = {})
8
+ @app = app
9
+ @options = options
10
+ end
11
+
12
+ def call(env)
13
+ app.call(env)
14
+ end
15
+
16
+ def service
17
+ @__service ||= _find_service
18
+ end
19
+
20
+ private
21
+
22
+ def _find_service
23
+ service = app
24
+
25
+ loop {
26
+ break if service.is_a?(Service)
27
+
28
+ [:app, :target].each { |meth|
29
+ if service.respond_to?(meth)
30
+ service = service.public_send(meth)
31
+ break
32
+ end
33
+ }
34
+ }
35
+
36
+ service.is_a?(Service) && service
37
+ end
38
+
39
+ def _error!(klass, message=nil)
40
+ Errors.throw_error!(klass, message)
41
+ end
42
+ end # Base
43
+ end # Middleware
44
+ end # Rester
@@ -0,0 +1,49 @@
1
+ require 'json'
2
+
3
+ module Rester
4
+ module Middleware
5
+ ##
6
+ # Provides error handling for Rester. Should be mounted above all other
7
+ # Rester middleware.
8
+ class ErrorHandling < Base
9
+ def call(env)
10
+ error = catch(:error) {
11
+ begin
12
+ return super
13
+ rescue Exception => error
14
+ throw :error, error
15
+ end
16
+ }
17
+
18
+ _error_to_response(error).finish
19
+ end
20
+
21
+ private
22
+
23
+ def _error_to_response(error)
24
+ Rack::Response.new(
25
+ [JSON.dump(message: error.message)],
26
+ _error_to_http_code(error),
27
+ { "Content-Type" => "application/json"}
28
+ )
29
+ end
30
+
31
+ def _error_to_http_code(error)
32
+ case error
33
+ when Errors::NotFoundError
34
+ 404
35
+ when Errors::ForbiddenError
36
+ 403
37
+ when Errors::AuthenticationError
38
+ 401
39
+ when Errors::RequestError
40
+ 400
41
+ when Errors::ServerError
42
+ 500
43
+ else
44
+ 500
45
+ end
46
+ end
47
+ end # ErrorHandling
48
+ end # Middleware
49
+ end # Rester
@@ -0,0 +1,12 @@
1
+ require 'rester'
2
+ require 'rails'
3
+
4
+ module Rester
5
+ class Railtie < Rails::Railtie
6
+ railtie_name :rester
7
+
8
+ rake_tasks do
9
+ Rester.load_tasks
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,137 @@
1
+ require 'uri'
2
+ require 'rack'
3
+
4
+ module Rester
5
+ class Service
6
+ ##
7
+ # The base set of middleware to use for every service.
8
+ # Middleware will be executed in the order specified.
9
+ BASE_MIDDLEWARE = [
10
+ Rack::Head,
11
+ Middleware::ErrorHandling
12
+ ].freeze
13
+
14
+ # Used to signify an empty body
15
+ class EmptyResponse; end
16
+
17
+ class << self
18
+ def instance
19
+ @instance ||= new
20
+ end
21
+
22
+ # The call method needs to call the rack_call method, which adds additional
23
+ # rack middleware.
24
+ def call(env)
25
+ instance.rack_call(env)
26
+ end
27
+
28
+ def method_missing(meth, *args, &block)
29
+ instance.public_send(meth, *args, &block)
30
+ end
31
+
32
+ ###
33
+ # Middleware DSL
34
+ ###
35
+
36
+ def use(klass, *args)
37
+ _middleware << [klass, *args]
38
+ end
39
+
40
+ def _middleware
41
+ @__middleware ||= BASE_MIDDLEWARE.dup
42
+ end
43
+ end # Class methods
44
+
45
+ attr_reader :request
46
+
47
+ def initialize(opts={})
48
+ @_opts = opts.dup
49
+ end
50
+
51
+ ##
52
+ # To be called by Rack. Wraps the app in middleware.
53
+ def rack_call(env)
54
+ _rack_app.call(env)
55
+ end
56
+
57
+ ##
58
+ # Call the service app directly.
59
+ #
60
+ # Duplicates the instance before processing the request so individual requests
61
+ # can't impact each other.
62
+ def call(env)
63
+ dup.call!(env)
64
+ end
65
+
66
+ ##
67
+ # Actually process the request.
68
+ #
69
+ # Calls methods that may modify instance variables, so the instance should
70
+ # be dup'd beforehand.
71
+ def call!(env)
72
+ @request = Rack::Request.new(env)
73
+ _process_request
74
+ end
75
+
76
+ ##
77
+ # Built in service method called by Client#connected?
78
+ def test_connection(params={})
79
+ end
80
+
81
+ private
82
+
83
+ def _rack_app
84
+ @__rack_app ||= _build_rack_app
85
+ end
86
+
87
+ def _build_rack_app
88
+ Rack::Builder.new.tap { |app|
89
+ self.class._middleware.each { |m| app.use(*m) }
90
+ app.run self
91
+ }.to_app
92
+ end
93
+
94
+ def _process_request
95
+ error!(Errors::NotFoundError) unless request.get? || request.post?
96
+ method, *args = _parse_path
97
+ params = _parse_params
98
+ method = "#{method}!" if request.post?
99
+ retval = public_send(method, *args, params)
100
+ _response(request.post? ? 201 : 200, _prepare_response(retval))
101
+ end
102
+
103
+ def _prepare_response(retval)
104
+ retval ||= {}
105
+
106
+ unless retval.is_a?(Hash)
107
+ error!(Errors::ServerError, "Invalid response: #{retval.inspect}")
108
+ end
109
+
110
+ JSON.dump(retval)
111
+ end
112
+
113
+ def _parse_path
114
+ path = request.path
115
+ uri = URI(path)
116
+ uri.path.split('/')[1..-1]
117
+ end
118
+
119
+ def _parse_params
120
+ if request.get?
121
+ request.GET
122
+ elsif request.post?
123
+ request.POST
124
+ end
125
+ end
126
+
127
+ def _response(status, body=EmptyResponse, headers={})
128
+ body = body == EmptyResponse ? [] : [body]
129
+ headers = headers.merge("Content-Type" => "application/json")
130
+ Rack::Response.new(body, status, headers).finish
131
+ end
132
+
133
+ def _error!(klass, message=nil)
134
+ Errors.throw_error!(klass, message)
135
+ end
136
+ end # Service
137
+ end # Rester
@@ -0,0 +1,49 @@
1
+ require 'date'
2
+
3
+ module Rester
4
+ module Utils
5
+ class << self
6
+ ##
7
+ # Determines the HTTP method/verb based on the method name.
8
+ # Defaults to GET but if the method ends with "!" it uses POST.
9
+ def extract_method_verb(meth)
10
+ meth = meth.to_s
11
+
12
+ if meth[-1] == '!'
13
+ [:post, meth[0..-2]]
14
+ else
15
+ [:get, meth]
16
+ end
17
+ end
18
+
19
+ def walk(object, context=nil, &block)
20
+ case object
21
+ when Hash
22
+ Hash[
23
+ object.map { |key, val|
24
+ [walk(key, :hash_key, &block), walk(val, :hash_value, &block)]
25
+ }
26
+ ]
27
+ when Array
28
+ object.map { |obj| walk(obj, :array_elem, &block) }
29
+ when Range
30
+ Range.new(
31
+ walk(object.begin, :range_begin, &block),
32
+ walk(object.end, :range_end, &block),
33
+ object.exclude_end?
34
+ )
35
+ else
36
+ yield object, context
37
+ end
38
+ end
39
+
40
+ def symbolize_keys(hash)
41
+ hash.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
42
+ end
43
+
44
+ def classify(str)
45
+ str.to_s.split("_").map(&:capitalize).join
46
+ end
47
+ end # Class methods
48
+ end # Utils
49
+ end # Rester
@@ -0,0 +1,3 @@
1
+ module Rester
2
+ VERSION = '0.0.1'
3
+ end
metadata ADDED
@@ -0,0 +1,136 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rester
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Robert Honer
8
+ - Kayvon Ghaffari
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2015-09-29 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rack
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '1.5'
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.5.2
24
+ type: :runtime
25
+ prerelease: false
26
+ version_requirements: !ruby/object:Gem::Requirement
27
+ requirements:
28
+ - - "~>"
29
+ - !ruby/object:Gem::Version
30
+ version: '1.5'
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.5.2
34
+ - !ruby/object:Gem::Dependency
35
+ name: rspec
36
+ requirement: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ type: :development
42
+ prerelease: false
43
+ version_requirements: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ - !ruby/object:Gem::Dependency
49
+ name: rspec-rails
50
+ requirement: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: sqlite3
64
+ requirement: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ type: :development
70
+ prerelease: false
71
+ version_requirements: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ - !ruby/object:Gem::Dependency
77
+ name: rails
78
+ requirement: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 4.0.0
83
+ type: :development
84
+ prerelease: false
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 4.0.0
90
+ description: A framework for creating simple RESTful interfaces between services.
91
+ email:
92
+ - robert@ribbonpayments.com
93
+ - kayvon@ribbon.co
94
+ executables: []
95
+ extensions: []
96
+ extra_rdoc_files: []
97
+ files:
98
+ - lib/rester.rb
99
+ - lib/rester/client.rb
100
+ - lib/rester/client/adapters.rb
101
+ - lib/rester/client/adapters/adapter.rb
102
+ - lib/rester/client/adapters/http_adapter.rb
103
+ - lib/rester/client/adapters/http_adapter/connection.rb
104
+ - lib/rester/errors.rb
105
+ - lib/rester/middleware.rb
106
+ - lib/rester/middleware/base.rb
107
+ - lib/rester/middleware/error_handling.rb
108
+ - lib/rester/railtie.rb
109
+ - lib/rester/service.rb
110
+ - lib/rester/utils.rb
111
+ - lib/rester/version.rb
112
+ homepage: http://github.com/ribbon/rester
113
+ licenses:
114
+ - BSD
115
+ metadata: {}
116
+ post_install_message:
117
+ rdoc_options: []
118
+ require_paths:
119
+ - lib
120
+ required_ruby_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ required_rubygems_version: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ version: '0'
130
+ requirements: []
131
+ rubyforge_project:
132
+ rubygems_version: 2.4.8
133
+ signing_key:
134
+ specification_version: 4
135
+ summary: A framework for creating simple RESTful interfaces between services.
136
+ test_files: []