resto 0.0.3 → 0.0.4
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.
- 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
|