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,66 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'net/https'
|
4
|
+
require 'resto/extra/delegation'
|
5
|
+
|
6
|
+
module Resto
|
7
|
+
module Request
|
8
|
+
class Factory
|
9
|
+
extend Resto::Extra::Delegation
|
10
|
+
|
11
|
+
delegate :read_host, :read_port, :options, :read_body, :composed_path,
|
12
|
+
:composed_headers, :scheme, :use_ssl, :to => :@request
|
13
|
+
|
14
|
+
def initialize(request)
|
15
|
+
@request = request
|
16
|
+
end
|
17
|
+
|
18
|
+
def head
|
19
|
+
http.start do |http|
|
20
|
+
http.request(Net::HTTP::Head.new(composed_path, composed_headers))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def get
|
25
|
+
http.start do |http|
|
26
|
+
http.request(Net::HTTP::Get.new(composed_path, composed_headers))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def post
|
31
|
+
http.start do |http|
|
32
|
+
http.request(Net::HTTP::Post.new(composed_path, composed_headers),
|
33
|
+
read_body)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def put
|
38
|
+
http.start do |http|
|
39
|
+
http.request(Net::HTTP::Put.new(composed_path, composed_headers),
|
40
|
+
read_body)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def delete
|
45
|
+
http.start do |http|
|
46
|
+
http.request(Net::HTTP::Delete.new(composed_path, composed_headers))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def http
|
51
|
+
::Net::HTTP.new(read_host, read_port).tap do |http|
|
52
|
+
|
53
|
+
use_ssl if scheme == "https"
|
54
|
+
|
55
|
+
unless options.keys.empty?
|
56
|
+
http.methods.grep(/\A(\w+)=\z/) do |meth|
|
57
|
+
key = $1.to_sym
|
58
|
+
options.key?(key) or next
|
59
|
+
http.__send__(meth, options[key])
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'resto/format'
|
3
|
+
|
4
|
+
require 'resto/extra/hash_args'
|
5
|
+
class BasicAuth < Resto::Extra::HashArgs
|
6
|
+
key :username
|
7
|
+
key :password
|
8
|
+
end
|
9
|
+
|
10
|
+
class FormatExtension < Resto::Extra::HashArgs
|
11
|
+
key :extension
|
12
|
+
end
|
13
|
+
|
14
|
+
module Resto
|
15
|
+
module Request
|
16
|
+
module Header
|
17
|
+
|
18
|
+
def format(symbol, options=nil)
|
19
|
+
formatter(Resto::Format.get(@symbol = symbol), options)
|
20
|
+
end
|
21
|
+
|
22
|
+
def formatter(formatter, options=nil)
|
23
|
+
@add_extension =
|
24
|
+
FormatExtension.new(options).fetch(:extension) { false }
|
25
|
+
@formatter = formatter
|
26
|
+
accept(formatter.accept)
|
27
|
+
content_type(formatter.content_type)
|
28
|
+
end
|
29
|
+
|
30
|
+
def composed_headers
|
31
|
+
@headers ||= { 'accept'=> '*/*' , 'user-agent'=> 'Ruby' }
|
32
|
+
end
|
33
|
+
|
34
|
+
def headers(headers)
|
35
|
+
tap { composed_headers.merge!(headers) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def accept(accept)
|
39
|
+
tap { composed_headers.store('accept', accept) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def content_type(content_type)
|
43
|
+
tap { composed_headers.store('content-type', content_type) }
|
44
|
+
end
|
45
|
+
|
46
|
+
def basic_auth(options)
|
47
|
+
options = BasicAuth.new(options)
|
48
|
+
|
49
|
+
username = options.fetch(:username)
|
50
|
+
password = options.fetch(:password)
|
51
|
+
base64_encode = ["#{username}:#{password}"].pack('m').delete("\r\n")
|
52
|
+
basic_encode = 'Basic ' + base64_encode
|
53
|
+
|
54
|
+
tap { composed_headers.store('authorization', basic_encode) }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
module Resto
|
6
|
+
module Request
|
7
|
+
module Option
|
8
|
+
|
9
|
+
def options
|
10
|
+
@options ||= {}
|
11
|
+
end
|
12
|
+
|
13
|
+
begin :ssl_attributes
|
14
|
+
|
15
|
+
def use_ssl(use_ssl=true)#
|
16
|
+
tap do
|
17
|
+
{ :verify_mode => OpenSSL::SSL::VERIFY_PEER }.update(options)
|
18
|
+
options.store(:use_ssl, use_ssl)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Sets the SSL version. See OpenSSL::SSL::SSLContext#ssl_version=
|
23
|
+
def ssl_version(ssl_version)#1.9
|
24
|
+
tap { options.store(:ssl_version, ssl_version) }
|
25
|
+
end
|
26
|
+
|
27
|
+
# Sets an OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object.
|
28
|
+
# (This method is appeared in Michal Rokos's OpenSSL extension.)
|
29
|
+
def key(key)#
|
30
|
+
tap { options.store(:key, key) }
|
31
|
+
end
|
32
|
+
|
33
|
+
# Sets an OpenSSL::X509::Certificate object as client certificate.
|
34
|
+
# (This method is appeared in Michal Rokos's OpenSSL extension).
|
35
|
+
def cert(cert)#
|
36
|
+
tap { options.store(:cert, cert) }
|
37
|
+
end
|
38
|
+
|
39
|
+
# Sets path of a CA certification file in PEM format.
|
40
|
+
# The file can contain several CA certificates.
|
41
|
+
def ca_file(ca_file)#
|
42
|
+
tap { options.store(:ca_file, ca_file) }
|
43
|
+
end
|
44
|
+
|
45
|
+
# Sets path of a CA certification directory containing certifications in
|
46
|
+
# PEM format.
|
47
|
+
def ca_path(ca_path)#
|
48
|
+
tap { options.store(:ca_path, ca_path) }
|
49
|
+
end
|
50
|
+
|
51
|
+
# Sets the X509::Store to verify peer certificate.
|
52
|
+
def cert_store(cert_store)#
|
53
|
+
tap { options.store(:cert_store, cert_store) }
|
54
|
+
end
|
55
|
+
|
56
|
+
# Sets the available ciphers. See OpenSSL::SSL::SSLContext#ciphers=
|
57
|
+
def ciphers(ciphers) # 1.9
|
58
|
+
tap { options.store(:ciphers, ciphers) }
|
59
|
+
end
|
60
|
+
|
61
|
+
# Sets the flags for server the certification verification at beginning of
|
62
|
+
# SSL/TLS session.
|
63
|
+
#
|
64
|
+
# OpenSSL::SSL::VERIFY_NONE or OpenSSL::SSL::VERIFY_PEER are acceptable.
|
65
|
+
def verify_mode(verify_mode)#
|
66
|
+
tap { options.store(:verify_mode, verify_mode) }
|
67
|
+
end
|
68
|
+
|
69
|
+
def verify_none
|
70
|
+
tap { verify_mode(OpenSSL::SSL::VERIFY_NONE) }
|
71
|
+
end
|
72
|
+
|
73
|
+
def verify_peer
|
74
|
+
tap { verify_mode(OpenSSL::SSL::VERIFY_PEER) }
|
75
|
+
end
|
76
|
+
|
77
|
+
# Sets the verify callback for the server certification verification.
|
78
|
+
def verify_callback(verify_callback)#
|
79
|
+
tap { options.store(:verify_callback, verify_callback)}
|
80
|
+
end
|
81
|
+
|
82
|
+
def verify_depth(verify_depth)#
|
83
|
+
tap { options.store(:verify_depth, verify_depth)}
|
84
|
+
end
|
85
|
+
|
86
|
+
# Sets the SSL timeout seconds.
|
87
|
+
def ssl_timeout(ssl_timeout)#
|
88
|
+
tap { options.store(:ssl_timeout, ssl_timeout)}
|
89
|
+
end
|
90
|
+
end #:ssl_attributes
|
91
|
+
|
92
|
+
def close_on_empty_response(close_on_empty_response)#
|
93
|
+
tap { options.store(:close_on_empty_response, close_on_empty_response) }
|
94
|
+
end
|
95
|
+
|
96
|
+
# Number of seconds to wait for the connection to open.
|
97
|
+
# If the HTTP object cannot open a connection in this many seconds,
|
98
|
+
# it raises a TimeoutError exception.
|
99
|
+
def open_timeout(time)#
|
100
|
+
tap { options.store(:open_timeout, time)}
|
101
|
+
end
|
102
|
+
|
103
|
+
# Number of seconds to wait for one block to be read (via one read(2) call).
|
104
|
+
# If the HTTP object cannot read data in this many seconds,
|
105
|
+
# it raises a TimeoutError exception.
|
106
|
+
def read_timeout(time)#
|
107
|
+
tap { options.store(:read_timeout, time)}
|
108
|
+
end
|
109
|
+
|
110
|
+
#Seconds to wait until reading one block (by one read(2) call).
|
111
|
+
def timeout(timeout) # 1.8.7 1.9??
|
112
|
+
tap { options.store(:timeout, timeout)}
|
113
|
+
end
|
114
|
+
|
115
|
+
# *WARNING* This method opens a serious security hole.
|
116
|
+
# Never use this method in production code.
|
117
|
+
#
|
118
|
+
# Sets an output stream for debugging.
|
119
|
+
#
|
120
|
+
# Resto.url('www.google.com').set_debug_output($stderr).get
|
121
|
+
def set_debug_output(output)
|
122
|
+
tap { options.store(:set_debug_output, output)}
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'uri'
|
3
|
+
require 'cgi'
|
4
|
+
|
5
|
+
module Resto
|
6
|
+
module Request
|
7
|
+
module Uri
|
8
|
+
def read_host
|
9
|
+
return nil unless @host
|
10
|
+
|
11
|
+
normalize_uri(@host)
|
12
|
+
@uri.host
|
13
|
+
end
|
14
|
+
|
15
|
+
def read_port; @port ||= 80; end
|
16
|
+
|
17
|
+
def composed_path
|
18
|
+
path = ["/#{@path}", @append_path].compact.join('/').gsub('//', "/")
|
19
|
+
path = "#{path}.#{current_formatter.extension}" if @add_extension
|
20
|
+
params = hash_to_params
|
21
|
+
return path unless (@query or params)
|
22
|
+
|
23
|
+
[path, [@query, params].compact.join('&')].join('?')
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :scheme
|
27
|
+
def normalize_uri(url)
|
28
|
+
@uri = URI.parse(url.match(/^https?:/) ? url : "http://#{url}")
|
29
|
+
@scheme ||= @uri.scheme
|
30
|
+
@host ||= @uri.host
|
31
|
+
@port ||= @uri.port
|
32
|
+
@path ||= @uri.path
|
33
|
+
@query ||= @uri.query
|
34
|
+
end
|
35
|
+
|
36
|
+
def parse_url(url)
|
37
|
+
normalize_uri(url)
|
38
|
+
end
|
39
|
+
|
40
|
+
def hash_to_params
|
41
|
+
return nil unless @params
|
42
|
+
raise ArgumentError unless @params.is_a?(Hash)
|
43
|
+
|
44
|
+
@params.sort.map do |a|
|
45
|
+
"#{CGI.escape(a.fetch(0).to_s)}=#{CGI.escape(a.fetch(1).to_s)}"
|
46
|
+
end.join('&')
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'resto/format'
|
4
|
+
require 'resto/translator/response_factory'
|
5
|
+
|
6
|
+
module Resto
|
7
|
+
module Response
|
8
|
+
class Base
|
9
|
+
|
10
|
+
def klass(klass)
|
11
|
+
tap { @klass = klass }
|
12
|
+
end
|
13
|
+
|
14
|
+
def translator(translator)
|
15
|
+
@translator = Resto::Translator::ResponseFactory.create(translator)
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
def format(symbol, options = {})
|
20
|
+
xpath = options[:xpath]
|
21
|
+
xpath(xpath) if xpath
|
22
|
+
formatter(Resto::Format.get(@symbol = symbol))
|
23
|
+
end
|
24
|
+
|
25
|
+
def xpath(xpath)
|
26
|
+
tap { @xpath = xpath }
|
27
|
+
end
|
28
|
+
|
29
|
+
def read_xpath
|
30
|
+
@xpath || "//#{@klass.to_s.downcase}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def formatter(formatter)
|
34
|
+
tap { @formatter = formatter }
|
35
|
+
end
|
36
|
+
|
37
|
+
def current_formatter
|
38
|
+
@formatter ||= Resto::Format.get(@symbol || :default)
|
39
|
+
end
|
40
|
+
|
41
|
+
def http_response(response)
|
42
|
+
tap { @response = response }
|
43
|
+
end
|
44
|
+
|
45
|
+
def read_body
|
46
|
+
body ? current_formatter.decode(body, :xpath => read_xpath) : nil
|
47
|
+
end
|
48
|
+
|
49
|
+
def body
|
50
|
+
@response ? @response.body : nil
|
51
|
+
end
|
52
|
+
|
53
|
+
def code
|
54
|
+
@response ? @response.code : nil
|
55
|
+
end
|
56
|
+
|
57
|
+
def valid?
|
58
|
+
(/\A20\d{1}\z/ =~ code.to_s) == 0
|
59
|
+
end
|
60
|
+
|
61
|
+
attr_reader :response
|
62
|
+
|
63
|
+
def to_object
|
64
|
+
return self unless @translator
|
65
|
+
|
66
|
+
@translator.call(@klass, read_body).tap do |instance|
|
67
|
+
instance.response = self
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_collection
|
72
|
+
return self unless @translator
|
73
|
+
|
74
|
+
body = read_body.is_a?(Hash) ? [read_body] : read_body
|
75
|
+
|
76
|
+
(body || []).map do |hash|
|
77
|
+
@translator.call(@klass, hash).tap do |instance|
|
78
|
+
instance.response = self
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Resto
|
4
|
+
module Translator
|
5
|
+
class Factory; end
|
6
|
+
|
7
|
+
class << Factory
|
8
|
+
def create(translator)
|
9
|
+
if translator.is_a?(Symbol) and :default == translator
|
10
|
+
new([])
|
11
|
+
elsif translator.is_a?(Array)
|
12
|
+
new(translator)
|
13
|
+
elsif translator.is_a?(Proc)
|
14
|
+
translator
|
15
|
+
elsif translator.is_a?(Class)
|
16
|
+
translator.new
|
17
|
+
else
|
18
|
+
raise(ArgumentError,
|
19
|
+
"Invalid argument. Valid symbol is :default. Array, Class or a
|
20
|
+
Proc is also a valid translator.")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Factory
|
26
|
+
def initialize(keys)
|
27
|
+
@keys = keys
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class RequestFactory < Factory
|
32
|
+
|
33
|
+
def call(attributes)
|
34
|
+
attributes ||= {}
|
35
|
+
raise ArgumentError unless attributes.is_a?(Hash)
|
36
|
+
|
37
|
+
@keys.reverse.inject(attributes) do |memo, item|
|
38
|
+
Hash.new.tap {|h| h[item] = memo }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'resto/translator/request_factory'
|
4
|
+
module Resto
|
5
|
+
module Translator
|
6
|
+
class ResponseFactory < Factory
|
7
|
+
|
8
|
+
def call(klass, hash)
|
9
|
+
hash ||= {}
|
10
|
+
raise ArgumentError unless hash.is_a?(Hash)
|
11
|
+
|
12
|
+
klass.new(traverse(hash, 0))
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def traverse(hash, index)
|
18
|
+
next_hash = hash[@keys.at(index)] || hash[@keys.at(index).to_s]
|
19
|
+
|
20
|
+
if next_hash
|
21
|
+
traverse(next_hash, index + 1)
|
22
|
+
else
|
23
|
+
hash
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|