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.
@@ -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,5 @@
1
+ require 'plangrade/resources/identity_map'
2
+ require 'plangrade/resources/base'
3
+ require 'plangrade/resources/user'
4
+ require 'plangrade/resources/company'
5
+ require 'plangrade/resources/participant'
@@ -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