plangrade-ruby 0.0.2
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.
- 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
|