stretchr 1.0.0
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/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
|