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,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
|