stretchr 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/stretchr/client.rb +201 -0
- data/lib/stretchr/configuration.rb +20 -0
- data/lib/stretchr/exceptions.rb +18 -0
- data/lib/stretchr/resources/resource.rb +129 -0
- data/lib/stretchr/resources.rb +1 -0
- data/lib/stretchr/security/signatory.rb +52 -0
- data/lib/stretchr/security.rb +1 -0
- data/lib/stretchr/stretchr_request.rb +16 -0
- data/lib/stretchr/stretchr_response.rb +39 -0
- data/lib/stretchr/transporters/default_transporter.rb +41 -0
- data/lib/stretchr/transporters/test_transporter.rb +25 -0
- data/lib/stretchr/transporters.rb +2 -0
- data/lib/stretchr.rb +32 -0
- data/test/test_client.rb +132 -0
- data/test/test_helper.rb +74 -0
- data/test/test_resources.rb +187 -0
- data/test/test_signatory.rb +67 -0
- data/test/test_stretchr_http_actions.rb +190 -0
- data/test/test_stretchr_request.rb +26 -0
- data/test/test_stretchr_response.rb +47 -0
- data/test/test_test_transporter.rb +42 -0
- metadata +66 -0
@@ -0,0 +1,201 @@
|
|
1
|
+
module Stretchr
|
2
|
+
|
3
|
+
class Client
|
4
|
+
def initialize(options = {})
|
5
|
+
options ||= {}
|
6
|
+
|
7
|
+
options[:project] ||= Stretchr.configuration.project
|
8
|
+
options[:private_key] ||= Stretchr.configuration.private_key
|
9
|
+
options[:public_key] ||= Stretchr.configuration.public_key
|
10
|
+
options[:noisy_errors] ||= (Stretchr.configuration.noisy_errors || false)
|
11
|
+
# check for required arguments
|
12
|
+
[:project, :public_key, :private_key].each do | required_option |
|
13
|
+
raise MissingAttributeError, "#{required_option} is required." unless options[required_option]
|
14
|
+
end
|
15
|
+
|
16
|
+
options.each do |name, value|
|
17
|
+
send("#{name}=", value)
|
18
|
+
end
|
19
|
+
|
20
|
+
# create defaults if the user didn't specify anything
|
21
|
+
@signatory ||= Stretchr::Signatory
|
22
|
+
@transporter ||= Stretchr::DefaultTransporter.new
|
23
|
+
@version ||= "v1"
|
24
|
+
@path ||= ""
|
25
|
+
@query = {}
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_accessor :project, :private_key, :public_key, :path, :http_method, :http_body, :version, :transporter, :signatory, :noisy_errors
|
30
|
+
|
31
|
+
#-------------------HTTP Actions-----------------
|
32
|
+
|
33
|
+
# generate_request makes a Stretchr::Request based on the current settings
|
34
|
+
# in this Stretchr object.
|
35
|
+
def generate_request
|
36
|
+
Stretchr::Request.new(
|
37
|
+
:http_method => http_method,
|
38
|
+
:signed_uri => signed_uri,
|
39
|
+
:body => http_body
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
def make_request!
|
44
|
+
# create and make the request
|
45
|
+
response = self.transporter.make_request(generate_request)
|
46
|
+
raise_errors_in_response(response) if noisy_errors
|
47
|
+
response
|
48
|
+
end
|
49
|
+
|
50
|
+
def raise_errors_in_response(response)
|
51
|
+
if [404, 500, 401, 403, 406, 400].include?(response.status)
|
52
|
+
raise_error(response.status)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def raise_error(status)
|
57
|
+
case status
|
58
|
+
when 404
|
59
|
+
raise NotFound
|
60
|
+
when 500
|
61
|
+
raise InternalServerError
|
62
|
+
when 401
|
63
|
+
raise Unauthorized
|
64
|
+
when 403
|
65
|
+
raise Forbidden
|
66
|
+
when 400
|
67
|
+
raise BadRequest
|
68
|
+
else
|
69
|
+
raise Unknown
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# get performs a GET request and returns the Stretchr::Response.
|
74
|
+
def get
|
75
|
+
|
76
|
+
self.http_method = :get
|
77
|
+
make_request!
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
# post performs a POST and returns a Stretch::Response
|
82
|
+
def post
|
83
|
+
self.http_method = :post
|
84
|
+
make_request!
|
85
|
+
end
|
86
|
+
|
87
|
+
def put
|
88
|
+
self.http_method = :put
|
89
|
+
make_request!
|
90
|
+
end
|
91
|
+
|
92
|
+
def delete
|
93
|
+
self.http_method = :delete
|
94
|
+
make_request!
|
95
|
+
end
|
96
|
+
|
97
|
+
#---------------- Friendly Actions --------------
|
98
|
+
|
99
|
+
def create(object)
|
100
|
+
self.body(object).post
|
101
|
+
end
|
102
|
+
|
103
|
+
def replace(object)
|
104
|
+
self.body(object).post
|
105
|
+
end
|
106
|
+
|
107
|
+
def update(object)
|
108
|
+
self.body(object).put
|
109
|
+
end
|
110
|
+
|
111
|
+
def read
|
112
|
+
self.get
|
113
|
+
end
|
114
|
+
|
115
|
+
#----------------Friendly Functions--------------
|
116
|
+
def url
|
117
|
+
uri.to_s
|
118
|
+
end
|
119
|
+
|
120
|
+
def to_url
|
121
|
+
url
|
122
|
+
end
|
123
|
+
|
124
|
+
def uri
|
125
|
+
URI::HTTP.build(host: "#{project}.stretchr.com", query: merge_query, path: merge_path)
|
126
|
+
end
|
127
|
+
|
128
|
+
def signed_uri
|
129
|
+
Stretchr::Signatory.generate_signed_url(http_method, uri, public_key, private_key, http_body)
|
130
|
+
end
|
131
|
+
|
132
|
+
#---------------Parameter Building---------------
|
133
|
+
|
134
|
+
def order(parameters)
|
135
|
+
@query["~order"] = parameters.to_s
|
136
|
+
self
|
137
|
+
end
|
138
|
+
|
139
|
+
def skip(parameters)
|
140
|
+
@query["~skip"] = parameters.to_i
|
141
|
+
self
|
142
|
+
end
|
143
|
+
|
144
|
+
def limit(parameters)
|
145
|
+
@query["~limit"] = parameters.to_i
|
146
|
+
self
|
147
|
+
end
|
148
|
+
|
149
|
+
def page(parameters)
|
150
|
+
skip((@query["~limit"] * parameters.to_i) - @query["~limit"])
|
151
|
+
self
|
152
|
+
end
|
153
|
+
|
154
|
+
def parameters(parameters)
|
155
|
+
@query.merge!(parameters)
|
156
|
+
self
|
157
|
+
end
|
158
|
+
|
159
|
+
def body(body_params)
|
160
|
+
self.http_body = body_params.to_json
|
161
|
+
|
162
|
+
self
|
163
|
+
end
|
164
|
+
|
165
|
+
def where(params)
|
166
|
+
params.each do |key, value|
|
167
|
+
@query["\:#{key.to_s}"] = value
|
168
|
+
end
|
169
|
+
self
|
170
|
+
end
|
171
|
+
|
172
|
+
#-----------------Basic Routing-----------------
|
173
|
+
|
174
|
+
def method_missing(method, *args)
|
175
|
+
add_collection(method, *args)
|
176
|
+
end
|
177
|
+
|
178
|
+
def add_collection(collection, id = nil)
|
179
|
+
@path += "/#{collection}"
|
180
|
+
if id
|
181
|
+
id.gsub!(/[^0-9A-Za-z.\-]/, '_') if id.is_a?(String) #remove non-ascii
|
182
|
+
@path += "/#{id}"
|
183
|
+
end
|
184
|
+
self
|
185
|
+
end
|
186
|
+
|
187
|
+
private
|
188
|
+
|
189
|
+
def merge_query
|
190
|
+
unless @query == nil || @query == {}
|
191
|
+
URI.encode_www_form(@query)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def merge_path
|
196
|
+
"/api/#{version}" + @path
|
197
|
+
end
|
198
|
+
|
199
|
+
end
|
200
|
+
|
201
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Stretchr
|
2
|
+
|
3
|
+
class Configuration
|
4
|
+
|
5
|
+
def self.add_option(name, default_value = nil)
|
6
|
+
attr_accessor name
|
7
|
+
@name = default_value
|
8
|
+
end
|
9
|
+
|
10
|
+
add_option :private_key
|
11
|
+
add_option :public_key
|
12
|
+
add_option :project
|
13
|
+
add_option :noisy_errors
|
14
|
+
|
15
|
+
def method_missing(name, *params)
|
16
|
+
raise Stretchr::UnknownConfiguration
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
#FIXME : Right now we just define some errors that users can implement if they wish. Should we implement them for them?
|
2
|
+
module Stretchr
|
3
|
+
#basic stretchr error namespace
|
4
|
+
class StretchrError < StandardError; end
|
5
|
+
|
6
|
+
#Configuration
|
7
|
+
class MissingAttributeError < StretchrError; end #thrown when initializing client without params
|
8
|
+
class UnknownConfiguration < StretchrError; end #thrown when we try to set an unknown configuration option
|
9
|
+
|
10
|
+
#stretchr status errors
|
11
|
+
class NotFound < StretchrError; end
|
12
|
+
class InternalServerError < StretchrError; end
|
13
|
+
class BadRequest < StretchrError; end
|
14
|
+
class Unathorized < StretchrError; end
|
15
|
+
class Forbidden < StretchrError; end
|
16
|
+
class Unknown < StretchrError; end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module Stretchr
|
2
|
+
class Resource
|
3
|
+
|
4
|
+
# Class Methods to find objects
|
5
|
+
|
6
|
+
def self.find(params = {})
|
7
|
+
stretchr = stretchr_client
|
8
|
+
#FIXME : Why does this need to be duplicated?
|
9
|
+
stretchr.path = prep_path(stretchr.path.dup, params)
|
10
|
+
response = stretchr.get
|
11
|
+
return false if !response.success?
|
12
|
+
self.new(response.data)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.all(params = {})
|
16
|
+
stretchr = stretchr_client
|
17
|
+
stretchr.path = prep_path(stretchr.path.dup, params)
|
18
|
+
response = stretchr.get
|
19
|
+
return [] if response.data["~c"] == 0 || !response.data["~i"]
|
20
|
+
response.data["~i"].map {|r| self.new(r) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.where(params = {})
|
24
|
+
stretchr = stretchr_client
|
25
|
+
#snag the vars that are needed for the path
|
26
|
+
path_vars = stretchr.path.scan(/:([a-zA-Z0-9_-]*)/i).flatten
|
27
|
+
#now convert the path as best we can
|
28
|
+
stretchr.path = prep_path(stretchr.path.dup, params)
|
29
|
+
#now remove the path params from the request
|
30
|
+
params.delete_if {|key, value| path_vars.include?(key.to_s)}
|
31
|
+
|
32
|
+
response = stretchr.where(params).get
|
33
|
+
#return false if nothing returned or search wasn't successful
|
34
|
+
return false if !response.success? || response.data["~c"] == 0 || !response.data["~i"]
|
35
|
+
response.data["~i"].map {|i| self.new(i) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.stretchr_client
|
39
|
+
Stretchr::Client.new(@config)
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.create(objects = [], params = {})
|
43
|
+
#convert it to an array for easy adding
|
44
|
+
objects = [objects] if !objects.is_a?(Array)
|
45
|
+
objects.map! {|o| setup_attributes_for_stretchr(o) }
|
46
|
+
stretchr = stretchr_client
|
47
|
+
stretchr.path = prep_path(stretchr.path.dup, params)
|
48
|
+
stretchr.body(objects)
|
49
|
+
response = stretchr.post
|
50
|
+
count = 0
|
51
|
+
stretchr_objects = objects.map do |o|
|
52
|
+
account = self.new(o)
|
53
|
+
account.parse_changes(response.changed[count])
|
54
|
+
count += 1
|
55
|
+
account
|
56
|
+
end
|
57
|
+
return stretchr_objects.first if stretchr_objects.length == 1
|
58
|
+
return stretchr_objects
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.stretchr_config(params = {})
|
62
|
+
@config ||= {}
|
63
|
+
@config.merge!(params)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Instance Methods
|
67
|
+
|
68
|
+
def initialize(params = {})
|
69
|
+
@attributes = {}
|
70
|
+
params.each {|key, value| self.send("#{key}=", value) }
|
71
|
+
end
|
72
|
+
|
73
|
+
def save(params = {})
|
74
|
+
stretchr = self.class.stretchr_client
|
75
|
+
stretchr.path = self.class.prep_path(stretchr.path.dup, params)
|
76
|
+
stretchr.body(setup_attributes_for_stretchr)
|
77
|
+
if self.stretchr_id
|
78
|
+
response = stretchr.put
|
79
|
+
else
|
80
|
+
response = stretchr.post
|
81
|
+
end
|
82
|
+
parse_changes(response.changed)
|
83
|
+
end
|
84
|
+
|
85
|
+
def parse_changes(response)
|
86
|
+
#FIXME : Should handle change objects and deltas here and update the object accordingly
|
87
|
+
return unless response
|
88
|
+
response.each {|key, value| self.send("#{key}=", value)}
|
89
|
+
self
|
90
|
+
end
|
91
|
+
|
92
|
+
def to_hash
|
93
|
+
@attributes
|
94
|
+
end
|
95
|
+
|
96
|
+
def to_json
|
97
|
+
to_hash.to_json
|
98
|
+
end
|
99
|
+
|
100
|
+
def method_missing(method, *args)
|
101
|
+
attribute = method.to_s
|
102
|
+
if attribute =~ /=$/ #if the method name ends in an =
|
103
|
+
@attributes[attribute.chop.gsub("~", "stretchr_")] = args[0]
|
104
|
+
else
|
105
|
+
@attributes[attribute]
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def self.prep_path(path, params = {})
|
112
|
+
params.each {|key, value| path.gsub!(":#{key.to_s}", value.to_s) unless value == nil}
|
113
|
+
#remove any unchanged params from the path
|
114
|
+
path.gsub!(/(:[a-zA-Z0-9_-]*)/, "")
|
115
|
+
path
|
116
|
+
end
|
117
|
+
|
118
|
+
def setup_attributes_for_stretchr
|
119
|
+
self.class.setup_attributes_for_stretchr(@attributes)
|
120
|
+
end
|
121
|
+
|
122
|
+
def self.setup_attributes_for_stretchr(attributes)
|
123
|
+
stretchr_attributes = {}
|
124
|
+
attributes.each_pair {|key, value| stretchr_attributes[key.to_s.gsub(/^stretchr_/, "~")] = value}
|
125
|
+
stretchr_attributes
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative 'resources/resource.rb'
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'uri' unless defined? URI
|
2
|
+
require 'cgi' unless defined? CGI
|
3
|
+
require 'digest/sha1'
|
4
|
+
|
5
|
+
module Stretchr
|
6
|
+
class Signatory
|
7
|
+
class << self
|
8
|
+
def generate_signed_url(method, uri, public_key, private_key, body = nil)
|
9
|
+
|
10
|
+
#we need a URI, let's make sure it's what we have
|
11
|
+
uri = URI.parse(URI.escape(uri)) unless uri.is_a?(URI)
|
12
|
+
|
13
|
+
#preparation
|
14
|
+
query = CGI.parse(uri.query || "")
|
15
|
+
query["~key"] = public_key
|
16
|
+
|
17
|
+
#store this for later, we'll need it
|
18
|
+
public_query = URI.encode_www_form(query)
|
19
|
+
|
20
|
+
#now add the private stuff
|
21
|
+
query["~private"] = private_key
|
22
|
+
query["~bodyhash"] = Digest::SHA1.hexdigest(body) unless body == nil
|
23
|
+
|
24
|
+
#sort it
|
25
|
+
query = sort_query(query)
|
26
|
+
uri.query = URI.encode_www_form(query)
|
27
|
+
|
28
|
+
#append the method
|
29
|
+
signature = generate_signature(method, uri.to_s)
|
30
|
+
|
31
|
+
#now we prepare it for public use
|
32
|
+
public_query = public_query + "&" unless public_query == nil
|
33
|
+
uri.query = public_query + URI.encode_www_form("~sign" => signature)
|
34
|
+
|
35
|
+
return uri
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def generate_signature(http_method, private_url)
|
41
|
+
combined = "#{http_method.to_s.upcase}&#{CGI.unescape(private_url.to_s)}"
|
42
|
+
Digest::SHA1.hexdigest(combined)
|
43
|
+
end
|
44
|
+
|
45
|
+
def sort_query(query)
|
46
|
+
query.each do |key, value|
|
47
|
+
value.sort! if value.is_a?(Array)
|
48
|
+
end.sort
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative 'security/signatory'
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Stretchr
|
2
|
+
class Request
|
3
|
+
|
4
|
+
attr_accessor :http_method, :body, :signed_uri, :headers
|
5
|
+
|
6
|
+
def initialize(options = {})
|
7
|
+
|
8
|
+
@http_method = options[:http_method]
|
9
|
+
@body = options[:body]
|
10
|
+
@signed_uri = options[:signed_uri]
|
11
|
+
@headers = options[:headers]
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'json' unless defined? JSON
|
2
|
+
module Stretchr
|
3
|
+
class Response
|
4
|
+
|
5
|
+
attr_reader :json_string, :json_object, :status, :client_context, :data, :changed, :errors, :raw_response
|
6
|
+
|
7
|
+
def initialize(options = nil)
|
8
|
+
|
9
|
+
options ||= {}
|
10
|
+
|
11
|
+
@raw_response = options[:response]
|
12
|
+
|
13
|
+
if options[:json]
|
14
|
+
|
15
|
+
# save the original json string
|
16
|
+
@json_string = options[:json]
|
17
|
+
@json_object = JSON.parse(@json_string)
|
18
|
+
|
19
|
+
@status = @json_object["~s"]
|
20
|
+
|
21
|
+
@client_context = @json_object["~x"]
|
22
|
+
@data = @json_object["~d"]
|
23
|
+
@changed = @json_object["~ch"]["~deltas"] if @json_object["~ch"]
|
24
|
+
|
25
|
+
unless @json_object["~e"].nil?
|
26
|
+
@errors = @json_object["~e"].collect {| error_obj | error_obj["~m"] }
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
# Gets whether this is a successful response.
|
34
|
+
def success?
|
35
|
+
@status >= 200 && @status <= 299
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require "uri" if !defined? URI
|
2
|
+
require "net/http" if !defined? Net
|
3
|
+
|
4
|
+
module Stretchr
|
5
|
+
class DefaultTransporter
|
6
|
+
|
7
|
+
def make_request(request)
|
8
|
+
response = nil
|
9
|
+
|
10
|
+
Net::HTTP.start(request.signed_uri.host, request.signed_uri.port) do |http|
|
11
|
+
|
12
|
+
http_request = generate_request(request)
|
13
|
+
response = http.request http_request # Net::HTTPResponse object
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
return Stretchr::Response.new({:response => response, :json => response.body})
|
18
|
+
end
|
19
|
+
|
20
|
+
def generate_request(request)
|
21
|
+
|
22
|
+
request_uri = request.signed_uri.request_uri
|
23
|
+
|
24
|
+
case request.http_method
|
25
|
+
when :get
|
26
|
+
req = Net::HTTP::Get.new request_uri
|
27
|
+
when :post
|
28
|
+
req = Net::HTTP::Post.new request_uri, {'Content-Type' => "application/json"}
|
29
|
+
req.body = request.body
|
30
|
+
req
|
31
|
+
when :put
|
32
|
+
req = Net::HTTP::Put.new request_uri, {'Content-Type' => "application/json"}
|
33
|
+
req.body = request.body
|
34
|
+
req
|
35
|
+
when :delete
|
36
|
+
req = Net::HTTP::Delete.new request_uri
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Stretchr
|
2
|
+
class TestTransporter
|
3
|
+
|
4
|
+
attr_accessor :requests, :responses
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
self.requests = []
|
8
|
+
self.responses = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def make_request(request)
|
12
|
+
|
13
|
+
# store the request and return the next response in the local queue
|
14
|
+
# not NOT actually make any http requests
|
15
|
+
|
16
|
+
self.requests << request
|
17
|
+
|
18
|
+
# return the response
|
19
|
+
self.responses.shift
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
data/lib/stretchr.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require "uri"
|
2
|
+
require "cgi"
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
|
6
|
+
module Stretchr
|
7
|
+
#exceptions that can be used by stretchr
|
8
|
+
require_relative 'stretchr/exceptions'
|
9
|
+
|
10
|
+
#configuration
|
11
|
+
require_relative 'stretchr/configuration'
|
12
|
+
|
13
|
+
@configuration = Stretchr::Configuration.new
|
14
|
+
|
15
|
+
def self.configuration
|
16
|
+
@configuration
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.config
|
20
|
+
yield(@configuration)
|
21
|
+
end
|
22
|
+
|
23
|
+
#the actual client library
|
24
|
+
require_relative 'stretchr/client'
|
25
|
+
require_relative 'stretchr/security'
|
26
|
+
require_relative 'stretchr/transporters'
|
27
|
+
require_relative 'stretchr/resources'
|
28
|
+
require_relative 'stretchr/stretchr_request'
|
29
|
+
require_relative 'stretchr/stretchr_response'
|
30
|
+
|
31
|
+
|
32
|
+
end
|