resto 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/blankslate.rb +110 -0
- data/lib/resto.rb +264 -0
- data/lib/resto/attributes.rb +56 -0
- data/lib/resto/extra/copy.rb +34 -0
- data/lib/resto/extra/delegation.rb +26 -0
- data/lib/resto/extra/hash_args.rb +56 -0
- data/lib/resto/format.rb +20 -0
- data/lib/resto/format/default.rb +11 -0
- data/lib/resto/format/json.rb +30 -0
- data/lib/resto/format/plain.rb +14 -0
- data/lib/resto/format/xml.rb +66 -0
- data/lib/resto/property.rb +50 -0
- data/lib/resto/property/handler.rb +57 -0
- data/lib/resto/property/integer.rb +29 -0
- data/lib/resto/property/string.rb +19 -0
- data/lib/resto/property/time.rb +43 -0
- data/lib/resto/request/base.rb +88 -0
- data/lib/resto/request/factory.rb +66 -0
- data/lib/resto/request/header.rb +58 -0
- data/lib/resto/request/option.rb +126 -0
- data/lib/resto/request/uri.rb +50 -0
- data/lib/resto/response/base.rb +85 -0
- data/lib/resto/translator/request_factory.rb +44 -0
- data/lib/resto/translator/response_factory.rb +28 -0
- data/lib/resto/validate.rb +37 -0
- data/lib/resto/validate/inclusion.rb +39 -0
- data/lib/resto/validate/length.rb +36 -0
- data/lib/resto/validate/presence.rb +24 -0
- data/lib/resto/version.rb +5 -0
- data/resto.gemspec +43 -0
- data/spec/resto/extra/copy_spec.rb +58 -0
- data/spec/resto/extra/hash_args_spec.rb +71 -0
- data/spec/resto/format/default_spec.rb +24 -0
- data/spec/resto/format/json_spec.rb +29 -0
- data/spec/resto/format/plain_spec.rb +21 -0
- data/spec/resto/format/xml_spec.rb +105 -0
- data/spec/resto/property/handler_spec.rb +57 -0
- data/spec/resto/property/integer_spec.rb +67 -0
- data/spec/resto/property/time_spec.rb +124 -0
- data/spec/resto/property_spec.rb +60 -0
- data/spec/resto/request/base_spec.rb +253 -0
- data/spec/resto/request/factory_spec.rb +114 -0
- data/spec/resto/translator/response_factory_spec.rb +93 -0
- data/spec/resto/validate/presence_spec.rb +102 -0
- data/spec/resto_spec.rb +531 -0
- data/spec/spec_helper.rb +119 -0
- metadata +48 -3
@@ -0,0 +1,26 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Resto
|
4
|
+
module Extra
|
5
|
+
module Delegation
|
6
|
+
|
7
|
+
def delegate(*methods)
|
8
|
+
options = methods.pop
|
9
|
+
to = options[:to]
|
10
|
+
unless options.is_a?(Hash) && to
|
11
|
+
raise ArgumentError, "Delegation needs a target. Supply an options
|
12
|
+
hash with a :to key as the last argument
|
13
|
+
(e.g. delegate :hello, :to => :greeter)."
|
14
|
+
end
|
15
|
+
|
16
|
+
methods.each do |method|
|
17
|
+
class_eval <<-EOS
|
18
|
+
def #{method}(*args, &block)
|
19
|
+
#{to}.__send__(#{method.inspect}, *args, &block)
|
20
|
+
end
|
21
|
+
EOS
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
class Resto::Extra::HashArgs; end
|
4
|
+
|
5
|
+
class << Resto::Extra::HashArgs
|
6
|
+
|
7
|
+
def key(key)
|
8
|
+
@keys ||= []
|
9
|
+
|
10
|
+
unless key.is_a?(Symbol)
|
11
|
+
raise ArgumentError, "The key '#{key}' must be a symbol"
|
12
|
+
end
|
13
|
+
|
14
|
+
if @keys.include?(key)
|
15
|
+
raise ArgumentError, "The key '#{key}' has already been defined."
|
16
|
+
end
|
17
|
+
|
18
|
+
@keys << key
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
def assert_key(key)
|
23
|
+
unless @keys.include?(key.to_sym)
|
24
|
+
raise ArgumentError, "The key '#{key}' is not valid.
|
25
|
+
Valid keys are: #{@keys.join(' ,')}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Resto::Extra::HashArgs
|
31
|
+
|
32
|
+
def initialize(hash)
|
33
|
+
hash ||= {}
|
34
|
+
raise ArgumentError, "'#{hash}' must be a Hash" unless hash.is_a?(Hash)
|
35
|
+
keys = hash.keys
|
36
|
+
keys_as_symbols = keys.map(&:to_sym)
|
37
|
+
if (keys_as_symbols.uniq.size != keys_as_symbols.size)
|
38
|
+
raise ArgumentError, "duplicated keys: #{keys.join(', ')}"
|
39
|
+
end
|
40
|
+
|
41
|
+
@hash = {}
|
42
|
+
keys.each do |key|
|
43
|
+
self.class.send(:assert_key, key)
|
44
|
+
@hash[key.to_sym] = hash.fetch(key)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def fetch(key, &block)
|
49
|
+
self.class.send(:assert_key, key)
|
50
|
+
@hash.fetch(key, &block)
|
51
|
+
end
|
52
|
+
|
53
|
+
def keys
|
54
|
+
@hash.keys
|
55
|
+
end
|
56
|
+
end
|
data/lib/resto/format.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'resto/format/default'
|
4
|
+
require 'resto/format/plain'
|
5
|
+
require 'resto/format/json'
|
6
|
+
require 'resto/format/xml'
|
7
|
+
|
8
|
+
module Resto
|
9
|
+
module Format
|
10
|
+
def self.get(symbol=:default)
|
11
|
+
format = Resto::Format.const_get("#{symbol.to_s.capitalize}")
|
12
|
+
end
|
13
|
+
|
14
|
+
def extension; end
|
15
|
+
def accept; '*/*'; end
|
16
|
+
def content_type; end
|
17
|
+
def encode(*args); args.first end
|
18
|
+
def decode(*args); args.first end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
# http://en.wikipedia.org/wiki/JSON
|
4
|
+
require 'yajl'
|
5
|
+
|
6
|
+
module Resto
|
7
|
+
module Format
|
8
|
+
class Json; end
|
9
|
+
|
10
|
+
class << Json
|
11
|
+
include Format
|
12
|
+
|
13
|
+
def extension; 'json'; end
|
14
|
+
def accept; 'application/json, */*'; end
|
15
|
+
def content_type; 'application/json'; end
|
16
|
+
|
17
|
+
def encode(hash, options = nil)
|
18
|
+
raise ArgumentError unless hash.is_a?(Hash)
|
19
|
+
|
20
|
+
Yajl::Encoder.encode(hash)
|
21
|
+
end
|
22
|
+
|
23
|
+
def decode(json, options=nil)
|
24
|
+
raise ArgumentError unless json.is_a?(String)
|
25
|
+
|
26
|
+
Yajl::Parser.parse(json)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
# https://tools.ietf.org/html/rfc3023
|
4
|
+
require 'resto/format'
|
5
|
+
require 'nokogiri'
|
6
|
+
|
7
|
+
module Resto
|
8
|
+
module Format
|
9
|
+
class Xml; end
|
10
|
+
|
11
|
+
class << Xml
|
12
|
+
include Format
|
13
|
+
|
14
|
+
def extension; 'xml'; end
|
15
|
+
def accept; 'application/xml, */*'; end
|
16
|
+
def content_type; 'application/xml;charset=utf-8'; end
|
17
|
+
|
18
|
+
def encode(hash, options = nil)
|
19
|
+
Nokogiri::XML::Builder.new { |xml| to_xml(hash, xml) }.to_xml
|
20
|
+
end
|
21
|
+
|
22
|
+
def decode(xml, options)
|
23
|
+
xpath = options.fetch(:xpath)
|
24
|
+
|
25
|
+
doc = Nokogiri::XML(xml)
|
26
|
+
nodes = doc.xpath(xpath)
|
27
|
+
|
28
|
+
case nodes.size
|
29
|
+
when 0
|
30
|
+
{}
|
31
|
+
when 1
|
32
|
+
elements_to_hash(nodes.first.children)
|
33
|
+
else
|
34
|
+
nodes.map { |node| elements_to_hash(node.children) }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def elements_to_hash(children)
|
41
|
+
attributes = {}
|
42
|
+
|
43
|
+
children.each do |element|
|
44
|
+
if element.is_a?(Nokogiri::XML::Element)
|
45
|
+
attributes[element.name] = element.text
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
attributes
|
50
|
+
end
|
51
|
+
|
52
|
+
#http://nokogiri.org/Nokogiri/XML/Builder.html
|
53
|
+
def to_xml(hash, xml)
|
54
|
+
hash.each do |key, value|
|
55
|
+
xml.send("#{key.to_s}_") do
|
56
|
+
if value.is_a?(Hash)
|
57
|
+
to_xml(value, xml)
|
58
|
+
else
|
59
|
+
xml.text value if value
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'resto/validate'
|
4
|
+
require 'resto/property/handler'
|
5
|
+
require 'resto/property/string'
|
6
|
+
require 'resto/property/integer'
|
7
|
+
require 'resto/property/time'
|
8
|
+
|
9
|
+
module Resto
|
10
|
+
module Property
|
11
|
+
|
12
|
+
def initialize(name, options={})
|
13
|
+
raise ':name must be a symbol' unless name.is_a?(Symbol)
|
14
|
+
|
15
|
+
@name = name
|
16
|
+
@remote_name = options.fetch(:remote_name) { name }
|
17
|
+
@validations = []
|
18
|
+
end
|
19
|
+
|
20
|
+
def remote_key
|
21
|
+
@remote_name.to_s
|
22
|
+
end
|
23
|
+
|
24
|
+
def attribute_key
|
25
|
+
@name
|
26
|
+
end
|
27
|
+
|
28
|
+
def attribute_key_as_string
|
29
|
+
@name.to_s
|
30
|
+
end
|
31
|
+
|
32
|
+
def validate_presence
|
33
|
+
Validate::Presence.new.tap { |validation| @validations.push(validation) }
|
34
|
+
end
|
35
|
+
|
36
|
+
def validate_inclusion
|
37
|
+
Validate::Inclusion.new.tap { |validation| @validations.push(validation) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def validate_length
|
41
|
+
Validate::Length.new.tap { |validation| @validations.push(validation) }
|
42
|
+
end
|
43
|
+
|
44
|
+
def validate(resource, attribute_key)
|
45
|
+
@validations.each do |validate|
|
46
|
+
validate.attribute_value(resource, attribute_key)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module Resto
|
3
|
+
module Property
|
4
|
+
class Handler
|
5
|
+
def initialize
|
6
|
+
@properties = {} # TODO fix indifferent access
|
7
|
+
@properties_with_indifferent_access = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def add(property)
|
11
|
+
@properties_with_indifferent_access.store(property.remote_key, property)
|
12
|
+
@properties_with_indifferent_access
|
13
|
+
.store(property.attribute_key, property)
|
14
|
+
@properties_with_indifferent_access
|
15
|
+
.store(property.attribute_key_as_string, property)
|
16
|
+
|
17
|
+
@properties.store(property.attribute_key, property)
|
18
|
+
end
|
19
|
+
|
20
|
+
def attribute_key(key)
|
21
|
+
get(key, 'attribute_key')
|
22
|
+
end
|
23
|
+
|
24
|
+
def remote_attributes(attributes)
|
25
|
+
remote_attributes = {}
|
26
|
+
|
27
|
+
attributes.each do |key, value|
|
28
|
+
remote_key = get(key, 'remote_key') || key
|
29
|
+
remote_attributes[remote_key] = value
|
30
|
+
end
|
31
|
+
|
32
|
+
remote_attributes
|
33
|
+
end
|
34
|
+
|
35
|
+
def cast(key, value, errors)
|
36
|
+
get(key).cast(value, errors)
|
37
|
+
end
|
38
|
+
|
39
|
+
def validate(resource)
|
40
|
+
@properties.each do |key, property|
|
41
|
+
property.validate(resource, key)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def get(key, method=nil)
|
47
|
+
property = @properties_with_indifferent_access.fetch(key, false)
|
48
|
+
|
49
|
+
if (property and method)
|
50
|
+
property.send(method)
|
51
|
+
else
|
52
|
+
property
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Resto
|
4
|
+
module Property
|
5
|
+
class Integer; end
|
6
|
+
|
7
|
+
class << Integer
|
8
|
+
end
|
9
|
+
|
10
|
+
class Integer
|
11
|
+
include Property
|
12
|
+
|
13
|
+
def initialize(name, options={})
|
14
|
+
@key = (name.to_s + "_integer").to_sym
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
def cast(value, errors)
|
19
|
+
errors.store(@key, nil)
|
20
|
+
|
21
|
+
begin
|
22
|
+
value.to_s.strip.empty? ? nil : Integer(value)
|
23
|
+
rescue ArgumentError, TypeError
|
24
|
+
nil.tap { errors.store(@key, ":#{attribute_key} is not an integer.") }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
module Resto
|
6
|
+
module Property
|
7
|
+
class Time; end
|
8
|
+
|
9
|
+
class << Time
|
10
|
+
end
|
11
|
+
|
12
|
+
class Time
|
13
|
+
include Property
|
14
|
+
|
15
|
+
def initialize(name, options={})
|
16
|
+
@key = (name.to_s + "_time").to_sym
|
17
|
+
super
|
18
|
+
end
|
19
|
+
|
20
|
+
def cast(value, errors)
|
21
|
+
errors.store(@key, nil)
|
22
|
+
|
23
|
+
formatted_value = value.to_s.strip
|
24
|
+
if formatted_value.gsub(/([a-z|A-Z]{1,5}\Z)/, '') =~ /[^T\d\-:\+\/\s]/
|
25
|
+
formatted_value = "invalid"
|
26
|
+
end
|
27
|
+
|
28
|
+
number_of_digits = formatted_value.gsub(/\D/, '').length
|
29
|
+
if (1..10).include?(number_of_digits)
|
30
|
+
formatted_value = "invalid"
|
31
|
+
end
|
32
|
+
|
33
|
+
begin
|
34
|
+
formatted_value.empty? ? nil : ::Time.parse(formatted_value)
|
35
|
+
rescue ArgumentError
|
36
|
+
nil.tap do
|
37
|
+
errors.store(@key, ":#{attribute_key} is not a valid time format.")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'resto/extra/delegation'
|
4
|
+
require 'resto/request/uri'
|
5
|
+
require 'resto/request/header'
|
6
|
+
require 'resto/request/option'
|
7
|
+
require 'resto/request/factory'
|
8
|
+
require 'resto/translator/request_factory'
|
9
|
+
require 'resto/extra/copy'
|
10
|
+
|
11
|
+
module Resto
|
12
|
+
module Request
|
13
|
+
class Base
|
14
|
+
extend Resto::Extra::Delegation
|
15
|
+
include Resto::Request::Header
|
16
|
+
include Resto::Request::Uri
|
17
|
+
include Resto::Request::Option
|
18
|
+
|
19
|
+
delegate :head, :get, :post, :put, :delete, :to => :@request
|
20
|
+
|
21
|
+
def initialize(request=Resto::Request::Factory)
|
22
|
+
@request_klass = request
|
23
|
+
@request = @request_klass.new(self)
|
24
|
+
end
|
25
|
+
|
26
|
+
def construct_path(options)
|
27
|
+
new_path = @path.clone
|
28
|
+
|
29
|
+
@path_substitute_keys.each do |substitute|
|
30
|
+
new_path.gsub!(/:#{substitute}/, options[substitute].to_s)
|
31
|
+
end
|
32
|
+
|
33
|
+
Extra::Copy.request_base(self).path(new_path)
|
34
|
+
end
|
35
|
+
|
36
|
+
def url(url)
|
37
|
+
tap { parse_url(url) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def host(host)
|
41
|
+
tap { @host = host }
|
42
|
+
end
|
43
|
+
|
44
|
+
def port(port)
|
45
|
+
tap { @port = port }
|
46
|
+
end
|
47
|
+
|
48
|
+
def path(path)
|
49
|
+
@path = path
|
50
|
+
keys = path.scan(/\/(:\w+)/).flatten
|
51
|
+
@path_substitute_keys = keys.map {|key| key.gsub(/:/, "").to_sym }
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
def append_path(append_path)
|
56
|
+
tap { @append_path = append_path }
|
57
|
+
end
|
58
|
+
|
59
|
+
def query(query)
|
60
|
+
tap { @query = query }
|
61
|
+
end
|
62
|
+
|
63
|
+
def params(hash)
|
64
|
+
tap { @params = hash }
|
65
|
+
end
|
66
|
+
|
67
|
+
def body(body)
|
68
|
+
tap { @body = body }
|
69
|
+
end
|
70
|
+
|
71
|
+
def translator(translator)
|
72
|
+
@translator = Resto::Translator::RequestFactory.create(translator)
|
73
|
+
self
|
74
|
+
end
|
75
|
+
|
76
|
+
def read_body
|
77
|
+
if @body
|
78
|
+
body = @translator ? @translator.call(@body) : @body
|
79
|
+
current_formatter.encode(body)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def current_formatter
|
84
|
+
@formatter ||= Resto::Format.get(@symbol || :default)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|