plangrade-ruby 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +3 -0
- data.tar.gz.sig +0 -0
- data/.gitignore +22 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/certs/public.pem +21 -0
- data/lib/plangrade.rb +31 -0
- data/lib/plangrade/api.rb +3 -0
- data/lib/plangrade/api/company.rb +25 -0
- data/lib/plangrade/api/participant.rb +25 -0
- data/lib/plangrade/api/user.rb +21 -0
- data/lib/plangrade/api_handler.rb +13 -0
- data/lib/plangrade/api_response.rb +43 -0
- data/lib/plangrade/client.rb +74 -0
- data/lib/plangrade/configurable.rb +67 -0
- data/lib/plangrade/error.rb +61 -0
- data/lib/plangrade/http_adapter.rb +86 -0
- data/lib/plangrade/oauth2_client.rb +79 -0
- data/lib/plangrade/resources.rb +5 -0
- data/lib/plangrade/resources/base.rb +196 -0
- data/lib/plangrade/resources/company.rb +19 -0
- data/lib/plangrade/resources/identity_map.rb +44 -0
- data/lib/plangrade/resources/participant.rb +24 -0
- data/lib/plangrade/resources/user.rb +25 -0
- data/lib/plangrade/ruby/version.rb +5 -0
- data/plangrade-ruby.gemspec +39 -0
- data/test/fixtures/one_user.yml +65 -0
- data/test/test_helper.rb +9 -0
- data/test/user/user_test.rb +19 -0
- metadata +267 -0
- metadata.gz.sig +2 -0
@@ -0,0 +1,61 @@
|
|
1
|
+
module Plangrade
|
2
|
+
module Error
|
3
|
+
|
4
|
+
class << self
|
5
|
+
def from_status(status=nil)
|
6
|
+
case status
|
7
|
+
when 400
|
8
|
+
BadRequest
|
9
|
+
when 401
|
10
|
+
Unauthorized
|
11
|
+
when 403
|
12
|
+
Forbidden
|
13
|
+
when 404
|
14
|
+
NotFound
|
15
|
+
when 406
|
16
|
+
NotAcceptable
|
17
|
+
when 429
|
18
|
+
RateLimitExceeded
|
19
|
+
when 500
|
20
|
+
InternalServerError
|
21
|
+
when 502
|
22
|
+
BadGateway
|
23
|
+
when 503
|
24
|
+
ServiceUnavailable
|
25
|
+
else
|
26
|
+
ApiError
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Raised when Plangrade returns unknown HTTP status code
|
32
|
+
class ApiError < StandardError; end
|
33
|
+
|
34
|
+
# Raised when Plangrade returns the HTTP status code 400
|
35
|
+
class BadRequest < ApiError; end
|
36
|
+
|
37
|
+
# Raised when Plangrade returns the HTTP status code 401
|
38
|
+
class Unauthorized < ApiError; end
|
39
|
+
|
40
|
+
# Raised when Plangrade returns the HTTP status code 403
|
41
|
+
class Forbidden < ApiError; end
|
42
|
+
|
43
|
+
# Raised when Plangrade returns the HTTP status code 404
|
44
|
+
class NotFound < ApiError; end
|
45
|
+
|
46
|
+
# Raised when Plangrade returns the HTTP status code 406
|
47
|
+
class NotAcceptable < ApiError; end
|
48
|
+
|
49
|
+
# Raised when Plangrade returns the HTTP status code 429
|
50
|
+
class RateLimitExceeded < ApiError; end
|
51
|
+
|
52
|
+
# Raised when Plangrade returns the HTTP status code 500
|
53
|
+
class InternalServerError < ApiError; end
|
54
|
+
|
55
|
+
# Raised when Plangrade returns the HTTP status code 502
|
56
|
+
class BadGateway < ApiError; end
|
57
|
+
|
58
|
+
# Raised when Plangrade returns the HTTP status code 503
|
59
|
+
class ServiceUnavailable < ApiError; end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'restclient'
|
2
|
+
require 'multi_json'
|
3
|
+
require 'addressable/uri'
|
4
|
+
|
5
|
+
module Plangrade
|
6
|
+
class HttpAdapter
|
7
|
+
|
8
|
+
def self.log=(output)
|
9
|
+
RestClient.log = output
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :site_url, :connection_options
|
13
|
+
|
14
|
+
def initialize(site_url, opts={})
|
15
|
+
unless site_url =~ /^https?/
|
16
|
+
raise ArgumentError, "site_url must include either http or https scheme"
|
17
|
+
end
|
18
|
+
@site_url = site_url
|
19
|
+
@connection_options = opts
|
20
|
+
end
|
21
|
+
|
22
|
+
# set the url to be used for creating an http connection
|
23
|
+
# @param url [string]
|
24
|
+
def site_url=(url)
|
25
|
+
@site_url = url
|
26
|
+
@host = nil
|
27
|
+
@scheme = nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def host
|
31
|
+
@host ||= parsed_url.host
|
32
|
+
end
|
33
|
+
|
34
|
+
def scheme
|
35
|
+
@scheme ||= parsed_url.scheme
|
36
|
+
end
|
37
|
+
|
38
|
+
def absolute_url(path='')
|
39
|
+
"#{@site_url}#{path}"
|
40
|
+
end
|
41
|
+
|
42
|
+
def connection_options=(opts)
|
43
|
+
raise ArgumentError, 'expected Hash' unless opts.is_a?(Hash)
|
44
|
+
@connection_options = opts
|
45
|
+
end
|
46
|
+
|
47
|
+
def send_request(method, path, opts={})
|
48
|
+
begin
|
49
|
+
params = opts.fetch(:params, {})
|
50
|
+
|
51
|
+
req_opts = self.connection_options.merge({
|
52
|
+
:method => method,
|
53
|
+
:headers => opts.fetch(:headers, {})
|
54
|
+
})
|
55
|
+
|
56
|
+
case method
|
57
|
+
when :get, :delete
|
58
|
+
query = Addressable::URI.form_encode(params)
|
59
|
+
normalized_path = query.empty? ? path : [path, query].join("?")
|
60
|
+
req_opts[:url] = absolute_url(normalized_path)
|
61
|
+
when :post, :put
|
62
|
+
req_opts[:payload] = params
|
63
|
+
req_opts[:url] = absolute_url(path)
|
64
|
+
else
|
65
|
+
raise "Unsupported HTTP method, #{method}"
|
66
|
+
end
|
67
|
+
|
68
|
+
resp = RestClient::Request.execute(req_opts)
|
69
|
+
|
70
|
+
result = Plangrade::ApiResponse.new(resp.headers, resp.body, resp.code)
|
71
|
+
rescue => e
|
72
|
+
if e.is_a?(RestClient::ExceptionWithResponse)
|
73
|
+
e.response
|
74
|
+
else
|
75
|
+
raise e
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
def parsed_url
|
82
|
+
Addressable::URI.parse(@site_url)
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'oauth2-client'
|
2
|
+
|
3
|
+
module Plangrade
|
4
|
+
class OAuth2Client < OAuth2Client::Client
|
5
|
+
|
6
|
+
SITE_URL = 'https://plangrade.com'
|
7
|
+
TOKEN_PATH = '/oauth/token'
|
8
|
+
AUTHORIZE_PATH = '/oauth/authorize'
|
9
|
+
|
10
|
+
def initialize(client_id, client_secret, opts={})
|
11
|
+
site_url = opts.delete(:site_url) || SITE_URL
|
12
|
+
opts[:token_path] ||= TOKEN_PATH
|
13
|
+
opts[:authorize_path] ||= AUTHORIZE_PATH
|
14
|
+
super(site_url, client_id, client_secret, opts)
|
15
|
+
yield self if block_given?
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
# Generates the Plangrade URL that the user will be redirected to in order to
|
20
|
+
# authorize your application
|
21
|
+
#
|
22
|
+
# @see http://docs.plangrade.com/#request-authorization
|
23
|
+
#
|
24
|
+
# @opts [Hash] additional parameters to be include in URL eg. scope, state, etc
|
25
|
+
#
|
26
|
+
# >> client = Plangrade::OAuth2Client.new('ETSIGVSxmgZitijWZr0G6w', '4bJZY38TCBB9q8IpkeualA2lZsPhOSclkkSKw3RXuE')
|
27
|
+
# >> client.webclient_authorization_url({
|
28
|
+
# :redirect_uri => 'http://localhost:3000/auth/plangrade/callback',
|
29
|
+
# })
|
30
|
+
# >> https://plangrade.com/oauth/authorize/?client_id={client_id}&
|
31
|
+
# redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fauth%2F%2Fplangrade%2Fcallback&response_type=token
|
32
|
+
#
|
33
|
+
def webclient_authorization_url(opts={})
|
34
|
+
implicit.token_url(opts)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Generates the Plangrade URL that the user will be redirected to in order to
|
38
|
+
# authorize your application
|
39
|
+
#
|
40
|
+
# @see http://docs.plangrade.com/#request-authorization
|
41
|
+
#
|
42
|
+
# @opts [Hash] additional parameters to be include in URL eg. scope, state, etc
|
43
|
+
#
|
44
|
+
# >> client = Plangrade::OAuth2Client.new('ETSIGVSxmgZitijWZr0G6w', '4bJZY38TCBB9q8IpkeualA2lZsPhOSclkkSKw3RXuE')
|
45
|
+
# >> client.webserver_authorization_url({
|
46
|
+
# :redirect_uri => 'http://localhost:3000/auth/plangrade/callback',
|
47
|
+
# })
|
48
|
+
# >> https://plangrade.com/oauth/authorize/?client_id={client_id}&
|
49
|
+
# redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Fplangrade%2Fcallback&response_type=code
|
50
|
+
#
|
51
|
+
def webserver_authorization_url(opts={})
|
52
|
+
opts[:scope] = normalize_scope(opts[:scope]) if opts[:scope]
|
53
|
+
authorization_code.authorization_url(opts)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Makes a request to Plangrade server that will swap your authorization code for an access
|
57
|
+
# token
|
58
|
+
#
|
59
|
+
# @see http://docs.plangrade.com/#finish-authorization
|
60
|
+
#
|
61
|
+
# @opts [Hash] may include redirect uri and other query parameters
|
62
|
+
#
|
63
|
+
# >> client = PlangradeClient.new(config)
|
64
|
+
# >> client.access_token_from_authorization_code('G3Y6jU3a', {
|
65
|
+
# :redirect_uri => 'http://localhost:3000/auth/plangrade/callback',
|
66
|
+
# })
|
67
|
+
#
|
68
|
+
# POST /oauth2/access_token HTTP/1.1
|
69
|
+
# Host: www.plangrade.com
|
70
|
+
# Content-Type: application/x-www-form-urlencoded
|
71
|
+
|
72
|
+
# client_id={client_id}&code=G3Y6jU3a&grant_type=authorization_code&
|
73
|
+
# redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Fplangrade%2Fcallback&client_secret={client_secret}
|
74
|
+
def access_token_from_authorization_code(code, opts={})
|
75
|
+
opts[:authenticate] ||= :body
|
76
|
+
authorization_code.get_token(code, opts)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
module Plangrade
|
2
|
+
module Resources
|
3
|
+
class Base
|
4
|
+
class << self
|
5
|
+
include ApiHandler
|
6
|
+
|
7
|
+
# Returns the non-qualified class name
|
8
|
+
# @!scope class
|
9
|
+
def base_name
|
10
|
+
@base_name ||= begin
|
11
|
+
word = "#{name.split(/::/).last}"
|
12
|
+
word.gsub!(/::/, '/')
|
13
|
+
word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
|
14
|
+
word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
|
15
|
+
word.tr!("-", "_")
|
16
|
+
word.downcase!
|
17
|
+
word
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Fetches JSON reprsentation for object model with provided `id`
|
22
|
+
# and returns a model instance with attributes
|
23
|
+
# @return [Yammer::Base]
|
24
|
+
# @param id [Integer]
|
25
|
+
# @!scope class
|
26
|
+
def get(id)
|
27
|
+
attrs = fetch(id)
|
28
|
+
attrs ? new(attrs) : nil
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
# @!scope class
|
33
|
+
def fetch(id)
|
34
|
+
return unless identity_map
|
35
|
+
attributes = identity_map.get("#{base_name}_#{id}")
|
36
|
+
unless attributes
|
37
|
+
result = api_handler.send("get_#{base_name}", id)
|
38
|
+
attributes = result.empty? ? nil : result.body
|
39
|
+
unless attributes.empty?
|
40
|
+
identity_map.put("#{base_name}_#{id}", attributes)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
attributes
|
44
|
+
end
|
45
|
+
|
46
|
+
# @!scope class
|
47
|
+
def identity_map
|
48
|
+
@identity_map ||= Plangrade::Resources::IdentityMap.new
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns a hash of all attributes that are meant to trigger an HTTP request
|
52
|
+
# @!scope class
|
53
|
+
def model_attributes
|
54
|
+
@model_attributes ||= {}
|
55
|
+
end
|
56
|
+
|
57
|
+
protected
|
58
|
+
|
59
|
+
def attr_accessor_deffered(*symbols)
|
60
|
+
symbols.each do |key|
|
61
|
+
# track attributes that should trigger a fetch
|
62
|
+
model_attributes[key] = false
|
63
|
+
|
64
|
+
# getter
|
65
|
+
define_method(key.to_s) do
|
66
|
+
load_deferred_attribute!(key)
|
67
|
+
instance_variable_get("@#{key}")
|
68
|
+
end
|
69
|
+
|
70
|
+
# setter
|
71
|
+
define_method("#{key}=") do |value|
|
72
|
+
load_deferred_attribute!(key)
|
73
|
+
if persisted? && loaded?
|
74
|
+
@modified_attributes[key] = value
|
75
|
+
else
|
76
|
+
@attrs[key] = value
|
77
|
+
end
|
78
|
+
instance_variable_set("@#{key}", value)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
attr_reader :id, :attrs
|
85
|
+
|
86
|
+
def initialize(props={})
|
87
|
+
@klass = self.class
|
88
|
+
@modified_attributes = {}
|
89
|
+
@new_record = true
|
90
|
+
@loaded = false
|
91
|
+
@attrs = props
|
92
|
+
self.id = @attrs.delete(:id)
|
93
|
+
self.update(@attrs)
|
94
|
+
|
95
|
+
yield self if block_given?
|
96
|
+
end
|
97
|
+
|
98
|
+
def api_handler
|
99
|
+
@klass.api_handler
|
100
|
+
end
|
101
|
+
|
102
|
+
def base_name
|
103
|
+
@klass.base_name
|
104
|
+
end
|
105
|
+
|
106
|
+
def new_record?
|
107
|
+
@new_record
|
108
|
+
end
|
109
|
+
|
110
|
+
def persisted?
|
111
|
+
!new_record?
|
112
|
+
end
|
113
|
+
|
114
|
+
def changes
|
115
|
+
@modified_attributes
|
116
|
+
end
|
117
|
+
|
118
|
+
def modified?
|
119
|
+
!changes.empty?
|
120
|
+
end
|
121
|
+
|
122
|
+
def loaded?
|
123
|
+
@loaded
|
124
|
+
end
|
125
|
+
|
126
|
+
def load!
|
127
|
+
@attrs = @klass.fetch(@id)
|
128
|
+
@loaded = true
|
129
|
+
update(@attrs)
|
130
|
+
self
|
131
|
+
end
|
132
|
+
|
133
|
+
def reload!
|
134
|
+
reset!
|
135
|
+
load!
|
136
|
+
end
|
137
|
+
|
138
|
+
def save
|
139
|
+
return self if ((persisted? && @modified_attributes.empty?) || @attrs.empty?)
|
140
|
+
|
141
|
+
result = if new_record?
|
142
|
+
api_handler.send("create_#{base_name}", @attrs)
|
143
|
+
else
|
144
|
+
api_handler.send("update_#{base_name}", @id, @modified_attributes)
|
145
|
+
end
|
146
|
+
@modified_attributes = {}
|
147
|
+
self
|
148
|
+
end
|
149
|
+
|
150
|
+
def delete!
|
151
|
+
return if new_record?
|
152
|
+
result = api_handler.send("delete_#{base_name}", @id)
|
153
|
+
result.success?
|
154
|
+
end
|
155
|
+
|
156
|
+
private
|
157
|
+
|
158
|
+
def id=(model_id)
|
159
|
+
return if model_id.nil?
|
160
|
+
@id = model_id.to_i
|
161
|
+
@new_record = false
|
162
|
+
end
|
163
|
+
|
164
|
+
# clear the entire class
|
165
|
+
def reset!
|
166
|
+
@modified_attributes = {}
|
167
|
+
@attrs = {}
|
168
|
+
@new_record = true
|
169
|
+
@loaded = false
|
170
|
+
end
|
171
|
+
|
172
|
+
protected
|
173
|
+
# loads model
|
174
|
+
def load_deferred_attribute!(key)
|
175
|
+
if @attrs.empty? && persisted? && !loaded?
|
176
|
+
load!
|
177
|
+
if !@attrs.has_key?(key)
|
178
|
+
raise "The key: #{key} appears not to be supported for model: #{self.base_name} \n #{@attrs.keys.inspect}"
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# set all fetchable attributes
|
184
|
+
def update(attrs={})
|
185
|
+
attrs.each do |key, value|
|
186
|
+
send("#{key}=", value) if self.respond_to?("#{key}=")
|
187
|
+
end
|
188
|
+
if persisted? && !loaded?
|
189
|
+
@loaded = @klass.model_attributes.keys.inject(true) do |result, key|
|
190
|
+
result && @attrs.has_key?(key)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Plangrade
|
2
|
+
module Resources
|
3
|
+
class Company < Plangrade::Resources::Base
|
4
|
+
|
5
|
+
def self.create(ein, name)
|
6
|
+
result = api_handler.create_company(:ein => ein, :name => name)
|
7
|
+
return nil unless result.created?
|
8
|
+
id = result.headers[:location].split('/').last.to_i
|
9
|
+
new(:id => id)
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_accessor_deffered :id, :name, :ein, :grade
|
13
|
+
|
14
|
+
def update!(params)
|
15
|
+
api_handler.update_company(@id, params)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|