rester 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f6fa57da0afc2d1f97912d9b52df6016e56fa1db
4
- data.tar.gz: 25570c5f460cb04b857003e6956e4b28d7f00968
3
+ metadata.gz: e0666455190175ce3fb1d58396d5504c036a3bd0
4
+ data.tar.gz: e06f3d2e16a01ef7c2c5161749136d68a95b9fda
5
5
  SHA512:
6
- metadata.gz: 3a7ac9757007d8e2bae0d4e31420dd9bc2c418c8bddd000d9e0b258d0161d029282e29963fdf7bcb27eba1340e97231c31af8a752336aaaa3b7d72a4db50c280
7
- data.tar.gz: 207559692ae6d064e1bcd3ec44938e7853197d88a894365ae4220dbdfcfde03b9ad254dca8bdfa2cbb2f58038baa133eb9208edf4cce44efa91df0857ccb631e
6
+ metadata.gz: 77529a26c51aa18efbe08814151905556863f41cc79bd45e3804ce1951a436df2786dc053bf445a7c5ad92968cd1fb3a3e170c875052e4dde141c9d864da1dad
7
+ data.tar.gz: 7f66c1970e7f6c98c462f682df91a79eb5c1687c746d52321b0b1188cb05879064bf5e526ab2a775ffb440fcfc1fcf8456500b04de418a7567843c570a7900fa
@@ -28,7 +28,7 @@ module Rester
28
28
  def request(verb, path, params={}, &block)
29
29
  params ||= {}
30
30
  _validate_verb(verb)
31
- _validate_params(params)
31
+ params = _validate_params(params)
32
32
  public_send("#{verb}!", path.to_s, params)
33
33
  end
34
34
 
@@ -62,48 +62,54 @@ module Rester
62
62
  delete: true
63
63
  }.freeze
64
64
 
65
- VALID_ARG_TYPES = {
66
- String => true,
67
- Symbol => true,
68
- Fixnum => true,
69
- Integer => true,
70
- Float => true
71
- }.freeze
72
-
73
- VALID_PARAM_KEY_TYPES = {
74
- String => true,
75
- Symbol => true
76
- }.freeze
65
+ ##
66
+ # PARAM_KEY_TRANSFORMERS
67
+ #
68
+ # Defines how to transform a key value before being sent to the server.
69
+ # At the moment, this is a simple to_s conversion.
70
+ PARAM_KEY_TRANSFORMERS = Hash.new { |_, key|
71
+ proc { |value|
72
+ fail ArgumentError, "Invalid param key type: #{key.inspect}"
73
+ }
74
+ }.merge(
75
+ String => :to_s.to_proc,
76
+ Symbol => :to_s.to_proc
77
+ ).freeze
77
78
 
78
- VALID_PARAM_VALUE_TYPES = {
79
- String => true,
80
- Symbol => true,
81
- Fixnum => true,
82
- Integer => true,
83
- Float => true,
84
- DateTime => true
85
- }.freeze
79
+ ##
80
+ # PARAM_VALUE_TRANSFORMERS
81
+ #
82
+ # Defines how values should be transformed before being sent to the
83
+ # server. Mostly, this is just a simple conversion to a string, but in
84
+ # the case of `nil` we want to convert it to 'null'.
85
+ PARAM_VALUE_TRANSFORMERS = Hash.new { |_, key|
86
+ proc { |value|
87
+ fail ArgumentError, "Invalid param value type: #{key.inspect}"
88
+ }
89
+ }.merge(
90
+ String => :to_s.to_proc,
91
+ Symbol => :to_s.to_proc,
92
+ Fixnum => :to_s.to_proc,
93
+ Integer => :to_s.to_proc,
94
+ Float => :to_s.to_proc,
95
+ DateTime => :to_s.to_proc,
96
+ TrueClass => :to_s.to_proc,
97
+ FalseClass => :to_s.to_proc,
98
+ NilClass => proc { 'null' }
99
+ ).freeze
86
100
 
87
101
  def _validate_verb(verb)
88
102
  VALID_VERBS[verb] or
89
103
  raise ArgumentError, "Invalid verb: #{verb.inspect}"
90
104
  end
91
105
 
92
- def _validate_args(args)
93
- args.each { |arg|
94
- VALID_ARG_TYPES[arg.class] or
95
- raise ArgumentError, "Invalid argument type: #{arg.inspect}"
96
- }
97
- end
98
-
99
106
  def _validate_params(params)
100
- params.each { |key, value|
101
- VALID_PARAM_KEY_TYPES[key.class] or
102
- raise ArgumentError, "Invalid param key type: #{key.inspect}"
103
-
104
- VALID_PARAM_VALUE_TYPES[value.class] or
105
- raise ArgumentError, "Invalid param value type: #{value.inspect}"
106
- }
107
+ params.map { |key, value|
108
+ [
109
+ PARAM_KEY_TRANSFORMERS[key.class].call(key),
110
+ PARAM_VALUE_TRANSFORMERS[value.class].call(value)
111
+ ]
112
+ }.to_h
107
113
  end
108
114
  end # Adapter
109
115
  end # Client::Adapters
@@ -0,0 +1,50 @@
1
+ require 'stringio'
2
+
3
+ module Rester
4
+ module Client::Adapters
5
+ ##
6
+ # An adapter for "connecting" to a service internally, without needing to
7
+ # interface over a HTTP connection.
8
+ class LocalAdapter < Adapter
9
+ attr_reader :service
10
+
11
+ def connect(service)
12
+ nil.tap { @service = service }
13
+ end
14
+
15
+ def connected?
16
+ !!service
17
+ end
18
+
19
+ def get!(path, params={})
20
+ _request(:get, path, headers: headers, query: params)
21
+ end
22
+
23
+ def delete!(path, params={})
24
+ _request(:delete, path, headers: headers, query: params)
25
+ end
26
+
27
+ def put!(path, params={})
28
+ _request(:put, path, headers: headers, data: params)
29
+ end
30
+
31
+ def post!(path, params={})
32
+ _request(:post, path, headers: headers, data: params)
33
+ end
34
+
35
+ private
36
+
37
+ def _request(verb, path, opts={})
38
+ body = URI.encode_www_form(opts[:data] || {})
39
+ query = URI.encode_www_form(opts[:query] || {})
40
+
41
+ service.call(
42
+ 'REQUEST_METHOD' => verb.to_s.upcase,
43
+ 'PATH_INFO' => path,
44
+ 'QUERY_STRING' => query,
45
+ 'rack.input' => StringIO.new(body)
46
+ ).tap { |r| r.delete_at(1) }
47
+ end
48
+ end # LocalAdapter
49
+ end # Client::Adapters
50
+ end # Rester
@@ -1,8 +1,9 @@
1
1
  module Rester
2
2
  class Client
3
3
  module Adapters
4
- autoload(:Adapter, 'rester/client/adapters/adapter')
5
- autoload(:HttpAdapter, 'rester/client/adapters/http_adapter')
4
+ autoload(:Adapter, 'rester/client/adapters/adapter')
5
+ autoload(:HttpAdapter, 'rester/client/adapters/http_adapter')
6
+ autoload(:LocalAdapter, 'rester/client/adapters/local_adapter')
6
7
  end
7
8
  end
8
9
  end
data/lib/rester/client.rb CHANGED
@@ -51,7 +51,7 @@ module Rester
51
51
  elsif status == 400
52
52
  raise Errors::RequestError, _parse_json(body)[:message]
53
53
  elsif status == 404
54
- raise Errors::NotFoundError, _parse_json(body)[:message]
54
+ raise Errors::NotFoundError, "/#{path}"
55
55
  else
56
56
  raise Errors::ServerError, _parse_json(body)[:message]
57
57
  end
data/lib/rester/errors.rb CHANGED
@@ -14,6 +14,8 @@ module Rester
14
14
  class MethodError < Error; end
15
15
  class MethodDefinitionError < Error; end
16
16
 
17
+ class ValidationError < Error; end
18
+
17
19
  #############
18
20
  # Http Errors
19
21
  class HttpError < Error; end
@@ -21,11 +21,19 @@ module Rester
21
21
  private
22
22
 
23
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
- )
24
+ code = _error_to_http_code(error)
25
+
26
+ unless code == 404
27
+ body_h = { message: error.message }
28
+
29
+ if code == 500
30
+ body_h[:error] = error.class.name
31
+ body_h[:backtrace] = error.backtrace
32
+ end
33
+ end
34
+
35
+ body = body_h ? [JSON.dump(body_h)] : []
36
+ Rack::Response.new(body, code, { "Content-Type" => "application/json"})
29
37
  end
30
38
 
31
39
  def _error_to_http_code(error)
@@ -0,0 +1,151 @@
1
+ module Rester
2
+ class Service::Object
3
+ class Validator
4
+ BASIC_TYPES = [String, Symbol, Float, Integer].freeze
5
+
6
+ attr_reader :options
7
+
8
+ def initialize(opts={})
9
+ @options = opts.dup.freeze
10
+ @_required_fields = []
11
+ @_all_fields = []
12
+
13
+ # Default "validator" is to just treat the param as a string.
14
+ @_validators = Hash.new([String, {}])
15
+ end
16
+
17
+ ##
18
+ # Whether or not validation will be done strictly (i.e., only specified
19
+ # params will be allowed).
20
+ def strict?
21
+ !!options[:strict]
22
+ end
23
+
24
+ def freeze
25
+ @_validators.freeze
26
+ @_required_fields.freeze
27
+ @_all_fields.freeze
28
+ end
29
+
30
+ def required_params
31
+ @_required_fields.dup
32
+ end
33
+
34
+ def validate(params)
35
+ param_keys = params.keys.map(&:to_sym)
36
+
37
+ unless (missing = @_required_fields - param_keys).empty?
38
+ _error!("missing params: #{missing.join(', ')}")
39
+ end
40
+
41
+ if strict? && !(unexpected = param_keys - @_all_fields).empty?
42
+ _error!("unexpected params: #{unexpected.join(', ')}")
43
+ end
44
+
45
+ params.map do |key, value|
46
+ [key.to_sym, validate!(key.to_sym, value)]
47
+ end.to_h
48
+ end
49
+
50
+ def validate!(key, value)
51
+ klass, opts = @_validators[key]
52
+
53
+ _parse_with_class(klass, value).tap do |obj|
54
+ if obj.nil? && @_required_fields.include?(key)
55
+ _error!("#{key} cannot be null")
56
+ end
57
+
58
+ opts.each do |opt, value|
59
+ case opt
60
+ when :within
61
+ _validate_within(key, obj, value)
62
+ else
63
+ _validate_method(key, obj, opt, value) unless obj.nil?
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ ##
70
+ # The basic data types all have helper methods named after them in Kernel.
71
+ # This allows you to do things like String(1234) to get '1234'. It's the
72
+ # same as doing 1234.to_s.
73
+ #
74
+ # Since methods already exist globally for these types, we need to override
75
+ # them so we can capture their calls. If this weren't the case, then we'd
76
+ # be catch them in `method_missing`.
77
+ BASIC_TYPES.each do |type|
78
+ define_method(type.to_s) { |name, opts={}|
79
+ _add_validator(name, type, opts)
80
+ }
81
+ end
82
+
83
+ ##
84
+ # Need to have special handling for Boolean since Ruby doesn't have a
85
+ # Boolean type, instead it has TrueClass and FalseClass...
86
+ def Boolean(name, opts={})
87
+ _add_validator(name, :boolean, opts)
88
+ end
89
+
90
+ private
91
+
92
+ def method_missing(meth, *args)
93
+ meth_str = meth.to_s
94
+
95
+ if meth.to_s.match(/\A[A-Z][A-Za-z]+\z/)
96
+ name = args.shift
97
+ opts = args.shift || {}
98
+ _add_validator(name, self.class.const_get(meth), opts)
99
+ end
100
+ end
101
+
102
+ def _add_validator(name, klass, opts)
103
+ fail 'must specify param name' unless name
104
+ fail 'validation options must be a Hash' unless opts.is_a?(Hash)
105
+ opts = opts.dup
106
+ @_required_fields << name.to_sym if opts.delete(:required)
107
+ @_all_fields << name.to_sym
108
+ @_validators[name.to_sym] = [klass, opts]
109
+ nil
110
+ end
111
+
112
+ def _parse_with_class(klass, value)
113
+ return nil if value == 'null'
114
+
115
+ if klass == String
116
+ value
117
+ elsif klass == Integer
118
+ value.to_i
119
+ elsif klass == Float
120
+ value.to_f
121
+ elsif klass == Symbol
122
+ value.to_sym
123
+ elsif klass == :boolean
124
+ value.downcase == 'true' ? true : false
125
+ else
126
+ klass.parse(value)
127
+ end
128
+ end
129
+
130
+ def _validate_within(key, obj, value)
131
+ unless value.include?(obj)
132
+ _error!("#{key} not within #{value.inspect}")
133
+ end
134
+ end
135
+
136
+ def _validate_method(key, obj, opt, value)
137
+ unless (meth = obj.respond_to?(opt) && obj.method(opt))
138
+ _error!("#{key} does not respond to #{opt.inspect}")
139
+ end
140
+
141
+ unless meth.call(*value)
142
+ _error!("#{key} failed #{opt}(#{[value].flatten.join(',')}) validation")
143
+ end
144
+ end
145
+
146
+ def _error!(message)
147
+ Errors.throw_error!(Errors::ValidationError, message)
148
+ end
149
+ end
150
+ end
151
+ end
@@ -4,6 +4,8 @@ require 'active_support/inflector'
4
4
  module Rester
5
5
  class Service
6
6
  class Object
7
+ autoload(:Validator, 'rester/service/object/validator')
8
+
7
9
  REQUEST_METHOD_TO_INSTANCE_METHOD = {
8
10
  'GET' => :get,
9
11
  'PUT' => :update,
@@ -33,7 +35,12 @@ module Rester
33
35
  def mount(klass)
34
36
  raise "Only other Service Objects can be mounted." unless klass < Object
35
37
  start = self.name.split('::')[0..-2].join('::').length + 2
36
- mounts[klass.name[start..-1].underscore] = klass
38
+ mounts[klass.name[start..-1].pluralize.underscore] = klass
39
+ end
40
+
41
+ def params(opts={}, &block)
42
+ (@_validator = Validator.new(opts)).instance_eval(&block)
43
+ @_validator.freeze
37
44
  end
38
45
  end # DSL
39
46
 
@@ -53,12 +60,17 @@ module Rester
53
60
  (@__mounts ||= {})
54
61
  end
55
62
 
63
+ def validator
64
+ @_validator ||= Validator.new
65
+ end
66
+
56
67
  ##
57
68
  # Helper method called at the class and instance level that calls the
58
69
  # specified method on the passed object with the params. Allows for
59
70
  # the arity of the method to be 0, 1 or -1.
60
71
  def process!(obj, meth, params)
61
- if obj.respond_to?(meth)
72
+ if meth && obj.respond_to?(meth)
73
+ params = validator.validate(params)
62
74
  meth = obj.method(meth)
63
75
 
64
76
  case meth.arity.abs
@@ -133,7 +133,7 @@ module Rester
133
133
  # If the version is not found, it throws a NotFoundError (HTTP 404).
134
134
  def _validate_version(request)
135
135
  unless self.class.versions.include?(request.version)
136
- _error!(Errors::NotFoundError)
136
+ _error!(Errors::NotFoundError, request.version)
137
137
  end
138
138
  end
139
139
 
@@ -1,3 +1,3 @@
1
1
  module Rester
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.0'
3
3
  end
data/lib/rester.rb CHANGED
@@ -17,7 +17,11 @@ module Rester
17
17
  end
18
18
 
19
19
  def connect(*args)
20
- Client.new(*args)
20
+ if (service = args.first).is_a?(Class) && service < Service
21
+ Client.new(Client::Adapters::LocalAdapter.new(service))
22
+ else
23
+ Client.new(*args)
24
+ end
21
25
  end
22
26
  end # Class Methods
23
27
  end # Rester
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rester
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Honer
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2015-10-04 00:00:00.000000000 Z
12
+ date: 2015-10-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rack
@@ -101,6 +101,20 @@ dependencies:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
103
  version: 4.0.0
104
+ - !ruby/object:Gem::Dependency
105
+ name: bundler-audit
106
+ requirement: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ type: :development
112
+ prerelease: false
113
+ version_requirements: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
104
118
  description: A framework for creating simple RESTful interfaces between services.
105
119
  email:
106
120
  - robert@payout.com
@@ -115,6 +129,7 @@ files:
115
129
  - lib/rester/client/adapters/adapter.rb
116
130
  - lib/rester/client/adapters/http_adapter.rb
117
131
  - lib/rester/client/adapters/http_adapter/connection.rb
132
+ - lib/rester/client/adapters/local_adapter.rb
118
133
  - lib/rester/client/resource.rb
119
134
  - lib/rester/errors.rb
120
135
  - lib/rester/middleware.rb
@@ -124,6 +139,7 @@ files:
124
139
  - lib/rester/railtie.rb
125
140
  - lib/rester/service.rb
126
141
  - lib/rester/service/object.rb
142
+ - lib/rester/service/object/validator.rb
127
143
  - lib/rester/service/request.rb
128
144
  - lib/rester/utils.rb
129
145
  - lib/rester/version.rb
@@ -147,7 +163,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
147
163
  version: '0'
148
164
  requirements: []
149
165
  rubyforge_project:
150
- rubygems_version: 2.4.8
166
+ rubygems_version: 2.4.6
151
167
  signing_key:
152
168
  specification_version: 4
153
169
  summary: A framework for creating simple RESTful interfaces between services.