elvanto-api 1.0.1
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
- data/Gemfile +9 -0
- data/Gemfile.lock +31 -0
- data/LICENSE.txt +22 -0
- data/README.md +85 -0
- data/Rakefile +2 -0
- data/elvanto.gemspec +26 -0
- data/lib/elvanto/client.rb +95 -0
- data/lib/elvanto/error.rb +65 -0
- data/lib/elvanto/pager.rb +222 -0
- data/lib/elvanto/resources/resource.rb +144 -0
- data/lib/elvanto/resources.rb +3 -0
- data/lib/elvanto/response/elvanto_exception_middleware.rb +40 -0
- data/lib/elvanto/utils.rb +89 -0
- data/lib/elvanto/version.rb +3 -0
- data/lib/elvanto.rb +102 -0
- metadata +137 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: eb813e51dbec03cf5916ca2c63eb97b88058611e
|
4
|
+
data.tar.gz: 1aff60cd74c8bb5c3994efce8100dd83b5abd679
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 047eaeeede24699e70c8abac9d8885c25f0c86b92915e611c789b04bbd063e7a35a190c73954ca2b63c278f806cdcc3dabe8151e4a7ecb1ef9730b19062ab210
|
7
|
+
data.tar.gz: 0da44c01c7ac066d80adde8fc7902a4cc6b264d489f157fb712fd5e5bb8a261fb753446413449fa4643fdc16fcf0630dbabb995980f928bd1f5aed262c31ea57
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
elvanto-api (1.0.1)
|
5
|
+
addressable (~> 2.3.5)
|
6
|
+
faraday (>= 0.8.6, <= 0.9.0)
|
7
|
+
faraday_middleware (~> 0.9.0)
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
addressable (2.3.8)
|
13
|
+
faraday (0.9.0)
|
14
|
+
multipart-post (>= 1.2, < 3)
|
15
|
+
faraday_middleware (0.9.1)
|
16
|
+
faraday (>= 0.7.4, < 0.10)
|
17
|
+
json (1.8.2)
|
18
|
+
multipart-post (2.0.0)
|
19
|
+
rake (10.4.2)
|
20
|
+
|
21
|
+
PLATFORMS
|
22
|
+
ruby
|
23
|
+
|
24
|
+
DEPENDENCIES
|
25
|
+
addressable
|
26
|
+
bundler (~> 1.7)
|
27
|
+
elvanto-api!
|
28
|
+
faraday
|
29
|
+
faraday_middleware
|
30
|
+
json
|
31
|
+
rake (~> 10.0)
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Elvanto
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
# Elvanto API Ruby Library
|
2
|
+
|
3
|
+
This library is all set to go with version 1 of the <a href="https://www.ElvantoAPI.com/api/" target="_blank">Elvanto API</a>.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'elvanto-api'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install elvanto-api
|
20
|
+
|
21
|
+
## Authenticating
|
22
|
+
|
23
|
+
The Elvanto API supports authentication using either <a href="https://www.ElvantoAPI.com/api/getting-started/#oauth" target="_blank">OAuth 2</a> or an <a href="https://www.ElvantoAPI.com/api/getting-started/#api_key" target="_blank">API key</a>.
|
24
|
+
|
25
|
+
### What is This For?
|
26
|
+
|
27
|
+
* Quick summary
|
28
|
+
This is an API wrapper to use in conjunction with an Elvanto account. This wrapper can be used by developers to develop programs for their own churches, or to design integrations to share to other churches using OAuth authentication.
|
29
|
+
* Version 1.0
|
30
|
+
|
31
|
+
### Using OAuth 2
|
32
|
+
|
33
|
+
This library provides functionality to help you obtain an Access Token and Refresh token. The first thing your application should do is redirect your user to the Elvanto authorization URL where they will have the opportunity to approve your application to access their Elvanto account. You can get this authorization URL by using the `authorize_url` method, like so:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
authorize_url = ElvantoAPI.authorize_url(client_id, redirect_uri, scope, state)
|
37
|
+
// Redirect your users to authorize_url.
|
38
|
+
```
|
39
|
+
|
40
|
+
If your user approves your application, they will then be redirected to the `redirect_uri` you specified, which will include a `code` parameter, and optionally a `state` parameter in the query string. Your application should implement a handler which can exchange the code passed to it for an access token, using `exchange_token` like so:
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
response = ElvantoAPI.exchange_token(client_id, client_secret, code, redirect_uri)
|
44
|
+
|
45
|
+
access_token = response[:access_token]
|
46
|
+
expires_in = response[:expires_in]
|
47
|
+
refresh_token = response[:refresh_token]
|
48
|
+
// Save access_token, expires_in and refresh_token.
|
49
|
+
```
|
50
|
+
|
51
|
+
At this point you have an access token and refresh token for your user which you should store somewhere convenient so that your application can look up these values when your user wants to make future Elvanto API calls.
|
52
|
+
|
53
|
+
Once you have an access token and refresh token for your user, you can authenticate and make further API calls like so:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
ElvantoAPI.configure({:access_token=>access_token})
|
57
|
+
all_people = ElvantoAPI.call("people/getAll")
|
58
|
+
```
|
59
|
+
|
60
|
+
All OAuth tokens have an expiry time, and can be renewed with a corresponding refresh token. If your access token expires when attempting to make an API call, you will receive an error response, so your code should handle this. Here's an example of how you could do this:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
result = ElvantoAPI.refresh_token(refresh_token)
|
64
|
+
```
|
65
|
+
|
66
|
+
### Using an API key
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
ElvantoAPI.configure({:api_key=>api_key})
|
70
|
+
people = ElvantoAPI.call("people/getAll")
|
71
|
+
```
|
72
|
+
|
73
|
+
## Documentation
|
74
|
+
|
75
|
+
Documentation can be found on the <a href="https://www.elvanto.com/api/" target="_blank">Elvanto API website</a>.
|
76
|
+
|
77
|
+
## Updates
|
78
|
+
|
79
|
+
Follow our <a href="http://twitter.com/ElvantoAPI" target="_blank">Twitter</a> to keep up-to-date with changes in the API.
|
80
|
+
|
81
|
+
## Support
|
82
|
+
|
83
|
+
For bugs with the API Ruby Wrapper please use the <a href="https://github.com/elvanto/elvanto-ruby/issues">Issue Tracker</a>.
|
84
|
+
|
85
|
+
For suggestions on the API itself, please <a href="http://support.ElvantoAPI.com/support/discussions/forums/1000123316" target="_blank">post in the forum</a> or contact us <a href="http://support.ElvantoAPI.com/support/tickets/new/" target="_blank">via our website</a>.
|
data/Rakefile
ADDED
data/elvanto.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'elvanto/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "elvanto-api"
|
8
|
+
spec.version = ElvantoAPI::VERSION
|
9
|
+
spec.authors = ["Elvanto"]
|
10
|
+
spec.email = ["support@elvanto.com"]
|
11
|
+
spec.summary = %q{Ruby wrapper for Elvanto API}
|
12
|
+
spec.description = %q{API wrapper for use in conjunction with an Elvanto account. This wrapper can be used by developers to develop programs for their own churches using an API Key, or to design integrations to share to other churches using OAuth authentication.}
|
13
|
+
spec.homepage = "https://www.elvanto.com"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
+
spec.add_dependency("faraday", ['>= 0.8.6', '<= 0.9.0'])
|
24
|
+
spec.add_dependency("faraday_middleware", '~> 0.9.0')
|
25
|
+
spec.add_dependency("addressable", '~> 2.3.5')
|
26
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'uri'
|
3
|
+
require 'faraday'
|
4
|
+
require 'faraday_middleware'
|
5
|
+
require 'response/elvanto_exception_middleware'
|
6
|
+
|
7
|
+
module ElvantoAPI
|
8
|
+
class Client
|
9
|
+
|
10
|
+
DEFAULTS = {
|
11
|
+
:scheme => 'http',
|
12
|
+
:host => 'localhost',
|
13
|
+
:api_version => nil,
|
14
|
+
:port => 3000,
|
15
|
+
:version => '1',
|
16
|
+
:logging_level => 'WARN',
|
17
|
+
:connection_timeout => 60,
|
18
|
+
:read_timeout => 60,
|
19
|
+
:logger => nil,
|
20
|
+
:ssl_verify => false,
|
21
|
+
:faraday_adapter => Faraday.default_adapter,
|
22
|
+
:accept_type => 'application/json'
|
23
|
+
}
|
24
|
+
|
25
|
+
attr_reader :conn
|
26
|
+
attr_accessor :access_token, :config
|
27
|
+
|
28
|
+
def initialize(options={})
|
29
|
+
@config = DEFAULTS.merge options
|
30
|
+
build_conn
|
31
|
+
end
|
32
|
+
|
33
|
+
def build_conn
|
34
|
+
if config[:logger]
|
35
|
+
logger = config[:logger]
|
36
|
+
else
|
37
|
+
logger = Logger.new(STDOUT)
|
38
|
+
logger.level = Logger.const_get(config[:logging_level].to_s)
|
39
|
+
end
|
40
|
+
|
41
|
+
Faraday::Response.register_middleware :handle_elvanto_errors => lambda { Faraday::Response::RaiseElvantoError }
|
42
|
+
|
43
|
+
options = {
|
44
|
+
:request => {
|
45
|
+
:open_timeout => config[:connection_timeout],
|
46
|
+
:timeout => config[:read_timeout]
|
47
|
+
},
|
48
|
+
:ssl => {
|
49
|
+
:verify => @config[:ssl_verify] # Only set this to false for testing
|
50
|
+
}
|
51
|
+
}
|
52
|
+
@conn = Faraday.new(url, options) do |cxn|
|
53
|
+
cxn.request :json
|
54
|
+
|
55
|
+
cxn.response :logger, logger
|
56
|
+
cxn.response :handle_elvanto_errors
|
57
|
+
cxn.response :json
|
58
|
+
#cxn.response :raise_error # raise exceptions on 40x, 50x responses
|
59
|
+
cxn.adapter config[:faraday_adapter]
|
60
|
+
end
|
61
|
+
conn.path_prefix = '/'
|
62
|
+
conn.headers['User-Agent'] = "elvanto-ruby/" + config[:version]
|
63
|
+
|
64
|
+
if config[:access_token]
|
65
|
+
# Authenticating with OAuth
|
66
|
+
conn.headers["Authorization"] = "Bearer " + config[:access_token]
|
67
|
+
elsif config[:api_key]
|
68
|
+
# Authenticating with an API key
|
69
|
+
conn.basic_auth(config[:api_key], '')
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
# Building the host url of the API Endpoint
|
75
|
+
def url
|
76
|
+
builder = (config[:scheme] == 'http') ? URI::HTTP : URI::HTTPS
|
77
|
+
builder.build({:host => config[:host],:port => config[:port],:scheme => config[:scheme]})
|
78
|
+
end
|
79
|
+
|
80
|
+
def api_version
|
81
|
+
return "" unless ElvantoAPI.config[:api_version]
|
82
|
+
ElvantoAPI.config[:api_version] + "/"
|
83
|
+
end
|
84
|
+
|
85
|
+
def post(href, options={})
|
86
|
+
uri = api_version + href + "." + config[:accept]
|
87
|
+
conn.post uri, options
|
88
|
+
end
|
89
|
+
|
90
|
+
def get(href, options={})
|
91
|
+
conn.get href, options
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module ElvantoAPI
|
2
|
+
|
3
|
+
# Custom error class for rescuing from all API response-related ElvantoAPI errors
|
4
|
+
class Error < ::StandardError
|
5
|
+
attr_reader :body
|
6
|
+
|
7
|
+
# @param [Hash] body The decoded json response body
|
8
|
+
def initialize(body=nil)
|
9
|
+
@body = Utils.indifferent_read_access(body)
|
10
|
+
unless body.nil?
|
11
|
+
super error_message
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
# @return [Sting] The error message containting in body.
|
17
|
+
def error_message
|
18
|
+
set_attrs
|
19
|
+
error = body.fetch('error', nil)
|
20
|
+
if error
|
21
|
+
error["message"]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
def set_attrs
|
27
|
+
error = body.fetch('error', nil)
|
28
|
+
unless error.nil?
|
29
|
+
error.keys.each do |name|
|
30
|
+
self.class.instance_eval {
|
31
|
+
define_method(name) { error[name] } # Get.
|
32
|
+
define_method("#{name}?") { !!error[name] } # Present.
|
33
|
+
}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# General error class for non API response exceptions
|
40
|
+
class StandardError < Error
|
41
|
+
attr_reader :message
|
42
|
+
alias :error_message :message
|
43
|
+
|
44
|
+
# @param [String, nil] message a description of the exception
|
45
|
+
def initialize(message = nil)
|
46
|
+
@message = message
|
47
|
+
super(message)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Raised when ElvantoAPI returns 50 or 102 error codes
|
52
|
+
class Unauthorized < Error; end
|
53
|
+
|
54
|
+
# Raised when ElvantoAPI returns a 250 error code
|
55
|
+
class BadRequest < Error; end
|
56
|
+
|
57
|
+
# Raised when ElvantoAPI returns a 404 error code
|
58
|
+
class NotFound < Error; end
|
59
|
+
|
60
|
+
# Raised when ElvantoAPI returns a 500 error code
|
61
|
+
class InternalError < Error; end
|
62
|
+
|
63
|
+
|
64
|
+
|
65
|
+
end
|
@@ -0,0 +1,222 @@
|
|
1
|
+
require "cgi"
|
2
|
+
|
3
|
+
module ElvantoAPI
|
4
|
+
class Pager
|
5
|
+
DEFAULT_SEP = /[&;] */n
|
6
|
+
DEFAULT_LIMIT = 10
|
7
|
+
|
8
|
+
include Enumerable
|
9
|
+
|
10
|
+
attr_accessor :href
|
11
|
+
attr_accessor :options
|
12
|
+
|
13
|
+
# A pager for paginating through resource records.
|
14
|
+
#
|
15
|
+
# @param [String] uri the uri of the resource
|
16
|
+
# @param [Hash] options
|
17
|
+
# @option options [Integer] limit
|
18
|
+
# @option options [Integer] offset
|
19
|
+
# @option options [Integer] per an alias for the :limit option
|
20
|
+
def initialize(href, options = {})
|
21
|
+
@href = href
|
22
|
+
@options = options
|
23
|
+
@page = nil
|
24
|
+
@resource_class = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def resource_class
|
28
|
+
return @resource_class unless @resource_class.nil?
|
29
|
+
load! unless @page
|
30
|
+
@resource_class
|
31
|
+
end
|
32
|
+
|
33
|
+
def first
|
34
|
+
load! unless @page
|
35
|
+
if items.first.nil?
|
36
|
+
nil
|
37
|
+
else
|
38
|
+
envelope = {
|
39
|
+
:meta => @page[:meta],
|
40
|
+
:links => @page[:links],
|
41
|
+
@resource_class.collection_name.to_sym => [items.first]
|
42
|
+
}
|
43
|
+
resource_class.construct_from_response(envelope)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def total
|
48
|
+
load! unless @page
|
49
|
+
@page[@resource_class.collection_name][:total]
|
50
|
+
end
|
51
|
+
|
52
|
+
def limit
|
53
|
+
load! unless @page
|
54
|
+
@page[@resource_class.collection_name][:per_page]
|
55
|
+
end
|
56
|
+
alias limit_value limit
|
57
|
+
|
58
|
+
|
59
|
+
def items
|
60
|
+
load! unless @page
|
61
|
+
if @resource_class.nil?
|
62
|
+
[]
|
63
|
+
else
|
64
|
+
@page[@resource_class.collection_name][@resource_class.member_name]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def current_page
|
69
|
+
@page[@resource_class.collection_name][:page]
|
70
|
+
end
|
71
|
+
|
72
|
+
def num_pages
|
73
|
+
num = total / limit
|
74
|
+
num += 1 if total % limit > 0
|
75
|
+
num
|
76
|
+
end
|
77
|
+
|
78
|
+
# @return [Array] Iterates through the current page of records.
|
79
|
+
# @yield [record]
|
80
|
+
def each
|
81
|
+
return enum_for :each unless block_given?
|
82
|
+
|
83
|
+
load! unless @page
|
84
|
+
loop do
|
85
|
+
items.each do |r|
|
86
|
+
envelope = {
|
87
|
+
@resource_class.member_name.to_sym => [r]
|
88
|
+
}
|
89
|
+
yield resource_class.construct_from_response(envelope)
|
90
|
+
end
|
91
|
+
raise StopIteration if last_page?
|
92
|
+
self.next
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def last_page?
|
97
|
+
current_page == num_pages
|
98
|
+
end
|
99
|
+
|
100
|
+
def first_page?
|
101
|
+
current_page == 1
|
102
|
+
end
|
103
|
+
|
104
|
+
# @return [nil]
|
105
|
+
# @see Resource.fetch_each
|
106
|
+
# @yield [record]
|
107
|
+
def fetch_each
|
108
|
+
return enum_for :fetch_each unless block_given?
|
109
|
+
begin
|
110
|
+
each { |record| yield record }
|
111
|
+
end while self.next
|
112
|
+
end
|
113
|
+
|
114
|
+
# @return [Array, nil] Refreshes the pager's collection of records with
|
115
|
+
# the next page.
|
116
|
+
def next
|
117
|
+
load! unless @page
|
118
|
+
|
119
|
+
new_options = @options.merge({page: current_page + 1})
|
120
|
+
load_from @href, new_options unless last_page?
|
121
|
+
end
|
122
|
+
|
123
|
+
# @return [Array, nil] Refreshes the pager's collection of records with
|
124
|
+
# the previous page.
|
125
|
+
def prev
|
126
|
+
load! unless @page
|
127
|
+
new_options = @options.merge({page: current_page - 1})
|
128
|
+
load_from @href, new_options unless first_page?
|
129
|
+
end
|
130
|
+
|
131
|
+
# @return [Array, nil] Refreshes the pager's collection of records with
|
132
|
+
# the first page.
|
133
|
+
def start
|
134
|
+
load! unless @page
|
135
|
+
new_options = @options.merge({page: 1})
|
136
|
+
load_from @href, new_options
|
137
|
+
end
|
138
|
+
|
139
|
+
# @return [Array, nil] Load (or reload) the pager's collection from the
|
140
|
+
# original, supplied options.
|
141
|
+
def load!
|
142
|
+
load_from @href, @options
|
143
|
+
end
|
144
|
+
alias reload load!
|
145
|
+
|
146
|
+
# @return [Pager] Duplicates the pager, updating it with the options
|
147
|
+
# supplied. Useful for resource scopes.
|
148
|
+
# @see #initialize
|
149
|
+
def paginate(options = {})
|
150
|
+
dup.instance_eval {
|
151
|
+
@page = nil
|
152
|
+
@options.update options and self
|
153
|
+
}
|
154
|
+
end
|
155
|
+
alias scoped paginate
|
156
|
+
alias where paginate
|
157
|
+
|
158
|
+
def all(options = {})
|
159
|
+
paginate(options).to_a
|
160
|
+
end
|
161
|
+
|
162
|
+
|
163
|
+
private
|
164
|
+
|
165
|
+
def load_from(uri, params)
|
166
|
+
parsed_uri = URI.parse(uri)
|
167
|
+
|
168
|
+
params ||= {}
|
169
|
+
params = params.dup
|
170
|
+
|
171
|
+
unless parsed_uri.query.nil?
|
172
|
+
# The reason we don't use CGI::parse here is because
|
173
|
+
# the ElvantoAPI api currently can't handle variable[]=value.
|
174
|
+
# Faraday ends up encoding a simple query string like:
|
175
|
+
# {"limit"=>["10"], "offset"=>["0"]}
|
176
|
+
# to limit[]=10&offset[]=0 and that's cool, but
|
177
|
+
# we have to make sure ElvantoAPI supports it.
|
178
|
+
query_params = parse_query(parsed_uri.query)
|
179
|
+
params.merge! query_params
|
180
|
+
parsed_uri.query = nil
|
181
|
+
end
|
182
|
+
|
183
|
+
response = ElvantoAPI.post parsed_uri.to_s, params
|
184
|
+
@page = ElvantoAPI::Utils.indifferent_read_access response.body
|
185
|
+
|
186
|
+
#@href = @page[:meta][:href]
|
187
|
+
# resource_class?
|
188
|
+
hypermedia_key = (@page.keys.map{|k| k.to_sym } - [:generated_in, :status]).first
|
189
|
+
|
190
|
+
unless hypermedia_key.nil?
|
191
|
+
@resource_class = ("ElvantoAPI::" + ElvantoAPI::Utils.classify(hypermedia_key)).constantize
|
192
|
+
end
|
193
|
+
|
194
|
+
@page
|
195
|
+
end
|
196
|
+
|
197
|
+
|
198
|
+
# Stolen from Mongrel, with some small modifications:
|
199
|
+
# Parses a query string by breaking it up at the '&'
|
200
|
+
# and ';' characters. You can also use this to parse
|
201
|
+
# cookies by changing the characters used in the second
|
202
|
+
# parameter (which defaults to '&;').
|
203
|
+
def parse_query(qs, d = nil)
|
204
|
+
params = {}
|
205
|
+
|
206
|
+
(qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
|
207
|
+
k, v = p.split('=', 2).map { |x| CGI::unescape(x) }
|
208
|
+
if (cur = params[k])
|
209
|
+
if cur.class == Array
|
210
|
+
params[k] << v
|
211
|
+
else
|
212
|
+
params[k] = [cur, v]
|
213
|
+
end
|
214
|
+
else
|
215
|
+
params[k] = v
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
params
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require File.expand_path('../../pager', __FILE__)
|
2
|
+
require File.expand_path('../../utils', __FILE__)
|
3
|
+
require 'addressable/template'
|
4
|
+
|
5
|
+
module ElvantoAPI
|
6
|
+
|
7
|
+
module Resource
|
8
|
+
|
9
|
+
|
10
|
+
attr_accessor :attributes
|
11
|
+
|
12
|
+
# @params [Hash] attributes List of object's attributes
|
13
|
+
def initialize(attributes = {})
|
14
|
+
@attributes = Utils.indifferent_read_access attributes
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return [Object] New copy of the object with updated attributes
|
18
|
+
def reload
|
19
|
+
self.class.find({id: id})
|
20
|
+
end
|
21
|
+
|
22
|
+
# @params [Symbol] method The name of the method to call
|
23
|
+
# @params [Hash] options The parameters to pass to the method.
|
24
|
+
# @return [Object] The response from the API method.
|
25
|
+
def query_member(method, options={})
|
26
|
+
self.class.query_member(method, options.merge({id: id}))
|
27
|
+
end
|
28
|
+
|
29
|
+
def method_missing(method, *args, &block)
|
30
|
+
if @attributes.has_key?(method.to_s)
|
31
|
+
return @attributes[method.to_s]
|
32
|
+
end
|
33
|
+
|
34
|
+
super method, *args, &block
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.included(base)
|
38
|
+
base.extend ClassMethods
|
39
|
+
end
|
40
|
+
|
41
|
+
module ClassMethods
|
42
|
+
|
43
|
+
def resource_name
|
44
|
+
Utils.demodulize name
|
45
|
+
end
|
46
|
+
|
47
|
+
def resource_class
|
48
|
+
name.constantize
|
49
|
+
end
|
50
|
+
|
51
|
+
def collection_name
|
52
|
+
Utils.pluralize Utils.underscore(resource_name)
|
53
|
+
end
|
54
|
+
|
55
|
+
def collection_path
|
56
|
+
collection_name
|
57
|
+
end
|
58
|
+
|
59
|
+
alias_method :href, :collection_path
|
60
|
+
|
61
|
+
def member_name
|
62
|
+
Utils.underscore resource_name
|
63
|
+
end
|
64
|
+
|
65
|
+
# @param [Symbol] payload Body of the API response
|
66
|
+
# @return [Object] Instance of the class specified in body
|
67
|
+
def construct_from_response(payload)
|
68
|
+
|
69
|
+
payload = ElvantoAPI::Utils.indifferent_read_access payload
|
70
|
+
# the remaining keys here are just hypermedia resources
|
71
|
+
payload.slice!(member_name)
|
72
|
+
|
73
|
+
instance = nil
|
74
|
+
|
75
|
+
payload.each do |key, value|
|
76
|
+
if value.class == Hash
|
77
|
+
resource_body = value
|
78
|
+
else
|
79
|
+
resource_body = value.first
|
80
|
+
end
|
81
|
+
# > Singular resources are represented as JSON objects. However,
|
82
|
+
# they are still wrapped inside an array:
|
83
|
+
#resource_body = value.first
|
84
|
+
cls = ("ElvantoAPI::" + ElvantoAPI::Utils.classify(key)).constantize
|
85
|
+
instance = cls.new resource_body
|
86
|
+
end
|
87
|
+
instance
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
# @param [Symbol] method The name of the method to call
|
92
|
+
# @return [Boolean] True if API method returns a single object, otherwise false
|
93
|
+
def member_method? method
|
94
|
+
return unless defined? resource_class::MEMBER_METHODS
|
95
|
+
resource_class::MEMBER_METHODS.keys.include? method
|
96
|
+
end
|
97
|
+
|
98
|
+
# @param [Symbol] method The name of the method to call
|
99
|
+
# @return [Boolean] True if API method returns a set of objects, otherwise false
|
100
|
+
def collection_method? method
|
101
|
+
return unless defined? resource_class::COLLECTION_METHODS
|
102
|
+
resource_class::COLLECTION_METHODS.keys.include? method
|
103
|
+
end
|
104
|
+
|
105
|
+
def method_missing(method, *args, &block)
|
106
|
+
if member_method? method
|
107
|
+
query_member(resource_class::MEMBER_METHODS[method], *args)
|
108
|
+
elsif collection_method? method
|
109
|
+
query_collection(resource_class::COLLECTION_METHODS[method], *args)
|
110
|
+
else
|
111
|
+
super method, *args, &block
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def def_instance_methods instance_methods={}
|
116
|
+
instance_methods.each do |key, value|
|
117
|
+
define_method(key) do |options={}|
|
118
|
+
self.query_member(value, options)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# @params [Symbol] method The name of the method to call
|
124
|
+
# @params [Hash] options The parameters to pass to the method.
|
125
|
+
# @return [Object] The response from the API method.
|
126
|
+
def query_member(method, options={})
|
127
|
+
uri = href + "/" + method.to_s
|
128
|
+
response = ElvantoAPI.post uri, options
|
129
|
+
construct_from_response response.body
|
130
|
+
end
|
131
|
+
|
132
|
+
# @params [Symbol] method The name of the method to call
|
133
|
+
# @params [Hash] options The parameters to pass to the method.
|
134
|
+
# @return [Array] The response from the API method.
|
135
|
+
def query_collection(method, options={})
|
136
|
+
uri = href + "/" + method.to_s
|
137
|
+
pager = ElvantoAPI::Pager.new uri, options
|
138
|
+
pager.to_a
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require_relative 'elvanto/error'
|
3
|
+
|
4
|
+
# @api private
|
5
|
+
module Faraday
|
6
|
+
|
7
|
+
class Response::RaiseElvantoError < Response::Middleware
|
8
|
+
|
9
|
+
CATEGORY_CODE_MAP = {
|
10
|
+
50 => ElvantoAPI::Unauthorized,
|
11
|
+
102 => ElvantoAPI::Unauthorized,
|
12
|
+
250 => ElvantoAPI::BadRequest,
|
13
|
+
404 => ElvantoAPI::NotFound,
|
14
|
+
500 => ElvantoAPI::InternalError
|
15
|
+
}
|
16
|
+
|
17
|
+
HTTP_STATUS_CODES = {
|
18
|
+
401 => ElvantoAPI::Unauthorized,
|
19
|
+
400 => ElvantoAPI::BadRequest,
|
20
|
+
404 => ElvantoAPI::NotFound,
|
21
|
+
500 => ElvantoAPI::InternalError
|
22
|
+
}
|
23
|
+
|
24
|
+
def on_complete(response)
|
25
|
+
|
26
|
+
status_code = response[:status].to_i
|
27
|
+
if response[:body] != nil && response[:body]['error']
|
28
|
+
category_code = response[:body]['error']["code"]
|
29
|
+
else
|
30
|
+
category_code = nil
|
31
|
+
end
|
32
|
+
|
33
|
+
error_class = CATEGORY_CODE_MAP[category_code] || HTTP_STATUS_CODES[status_code]
|
34
|
+
raise error_class.new(response[:body]) if error_class
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module ElvantoAPI
|
2
|
+
CLASS_MAPPING = {}
|
3
|
+
|
4
|
+
module Utils
|
5
|
+
|
6
|
+
|
7
|
+
|
8
|
+
def callable( callable_or_not )
|
9
|
+
callable_or_not.respond_to?(:call) ? callable_or_not : lambda { callable_or_not }
|
10
|
+
end
|
11
|
+
|
12
|
+
def camelize(underscored_word)
|
13
|
+
underscored_word.to_s.gsub(/(?:^|_)(.)/) { $1.upcase }
|
14
|
+
end
|
15
|
+
|
16
|
+
def classify(table_name)
|
17
|
+
class_name = camelize singularize(table_name.to_s.sub(/.*\./, ''))
|
18
|
+
class_name = CLASS_MAPPING[class_name] if CLASS_MAPPING.keys.include? class_name
|
19
|
+
return class_name
|
20
|
+
end
|
21
|
+
|
22
|
+
def demodulize(class_name_in_module)
|
23
|
+
class_name_in_module.to_s.sub(/^.*::/, '')
|
24
|
+
end
|
25
|
+
|
26
|
+
def pluralize(word)
|
27
|
+
return "people" if word == "person"
|
28
|
+
word.to_s.pluralize
|
29
|
+
end
|
30
|
+
|
31
|
+
def singularize(word)
|
32
|
+
return "person" if word == "people"
|
33
|
+
word.to_s.sub(/s$/, '').sub(/ie$/, 'y')
|
34
|
+
end
|
35
|
+
|
36
|
+
def underscore(camel_cased_word)
|
37
|
+
word = camel_cased_word.to_s.dup
|
38
|
+
word.gsub!(/::/, '/')
|
39
|
+
word.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
40
|
+
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
41
|
+
word.tr! '-', '_'
|
42
|
+
word.downcase!
|
43
|
+
word
|
44
|
+
end
|
45
|
+
|
46
|
+
def extract_href_from_object(object)
|
47
|
+
object.respond_to?(:href) ? object.href : object
|
48
|
+
end
|
49
|
+
|
50
|
+
def indifferent_read_access(base = {})
|
51
|
+
indifferent = Hash.new do |hash, key|
|
52
|
+
hash[key.to_s] if key.is_a? Symbol
|
53
|
+
end
|
54
|
+
base.each_pair do |key, value|
|
55
|
+
if value.is_a? Hash
|
56
|
+
value = indifferent_read_access value
|
57
|
+
elsif value.respond_to? :each
|
58
|
+
if value.respond_to? :map!
|
59
|
+
value.map! do |v|
|
60
|
+
if v.is_a? Hash
|
61
|
+
v = indifferent_read_access v
|
62
|
+
end
|
63
|
+
v
|
64
|
+
end
|
65
|
+
else
|
66
|
+
value.map do |v|
|
67
|
+
if v.is_a? Hash
|
68
|
+
v = indifferent_read_access v
|
69
|
+
end
|
70
|
+
v
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
indifferent[key.to_s] = value
|
75
|
+
end
|
76
|
+
indifferent
|
77
|
+
end
|
78
|
+
|
79
|
+
def stringify_keys!(hash)
|
80
|
+
hash.keys.each do |key|
|
81
|
+
stringify_keys! hash[key] if hash[key].is_a? Hash
|
82
|
+
hash[key.to_s] = hash.delete key if key.is_a? Symbol
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
extend self
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
data/lib/elvanto.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
$:.unshift File.join(File.dirname(__FILE__), "elvanto", "resources")
|
2
|
+
$:.unshift File.join(File.dirname(__FILE__), "elvanto", "response")
|
3
|
+
|
4
|
+
require 'uri'
|
5
|
+
require 'elvanto/version' unless defined? ElvantoAPI::VERSION
|
6
|
+
require 'elvanto/client'
|
7
|
+
require 'elvanto/utils'
|
8
|
+
require 'elvanto/error'
|
9
|
+
|
10
|
+
|
11
|
+
module ElvantoAPI
|
12
|
+
|
13
|
+
@config = {
|
14
|
+
:scheme => 'https',
|
15
|
+
:host => 'api.elvanto.com',
|
16
|
+
:port => 443,
|
17
|
+
:version => '1',
|
18
|
+
:api_version => "/v1",
|
19
|
+
:accept => "json"
|
20
|
+
}
|
21
|
+
|
22
|
+
@tokens = {}
|
23
|
+
|
24
|
+
@hypermedia_registry = {}
|
25
|
+
|
26
|
+
class << self
|
27
|
+
|
28
|
+
attr_accessor :client
|
29
|
+
attr_accessor :config
|
30
|
+
attr_accessor :tokens
|
31
|
+
|
32
|
+
|
33
|
+
# @params [Hash] options Connection configuration options
|
34
|
+
# In order to authenticate with access token: options = {access_token: "access_token", ...}
|
35
|
+
# In order to authenticate with API key: options = {api_key: "api_key", ...}
|
36
|
+
def configure(options={})
|
37
|
+
@config = @config.merge(options)
|
38
|
+
@config[:access_token] ||= tokens[:access_token]
|
39
|
+
@client = ElvantoAPI::Client.new(@config)
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
|
44
|
+
# @params [String] client_id The Client ID of your registered OAuth application.
|
45
|
+
# @params [String] redirect_url The Redirect URI of your registered OAuth application.
|
46
|
+
# @params [String] scope
|
47
|
+
# @params [String] state Optional state data to be included in the URL.
|
48
|
+
# @return [String] The authorization URL to which users of your application should be redirected.
|
49
|
+
def authorize_url(client_id, redirect_uri, scope="AdministerAccount", state=nil)
|
50
|
+
|
51
|
+
scope = scope.join(",") if scope.class == Array
|
52
|
+
|
53
|
+
params = {type: "web_server", client_id: client_id, redirect_uri: redirect_uri, scope: scope}
|
54
|
+
params[:state] = state if state
|
55
|
+
|
56
|
+
uri = Addressable::URI.new({:host => config[:host],:scheme => config[:scheme], :path => "oauth",:query_values => params})
|
57
|
+
return uri.to_s
|
58
|
+
end
|
59
|
+
|
60
|
+
# @params [String] client_id The Client ID of your registered OAuth application.
|
61
|
+
# @param [String] client_secret The Client Secret of your registered OAuth application.
|
62
|
+
# @param [String] code The unique OAuth code to be exchanged for an access token.
|
63
|
+
# @param [String] redirect_url The Redirect URI of your registered OAuth application.
|
64
|
+
# @return [Hash] The hash with keys 'access_token', 'expires_in', and 'refresh_token'
|
65
|
+
def exchange_token(client_id, client_secret, code, redirect_uri)
|
66
|
+
params = {grant_type: 'authorization_code', client_id: client_id, client_secret: client_secret, code: code, redirect_uri: redirect_uri}
|
67
|
+
response = Faraday.new(client.url).post "oauth/token", params
|
68
|
+
@tokens = JSON.parse(response.body, :symbolize_keys => true)
|
69
|
+
return @tokens
|
70
|
+
end
|
71
|
+
|
72
|
+
# @param [String] refresh_token Was included when the original token was granted to automatically retrieve a new access token.
|
73
|
+
# @return [Hash] The hash with keys 'access_token', 'expires_in', and 'refresh_token'
|
74
|
+
def refresh_token(token=nil)
|
75
|
+
token ||= tokens[:refresh_token]
|
76
|
+
raise "Error refreshing token. There is no refresh token set on this object" unless token
|
77
|
+
|
78
|
+
params = {grant_type: "refresh_token", refresh_token: token}
|
79
|
+
response = Faraday.new(client.url).post "oauth/token", params
|
80
|
+
@tokens = JSON.parse(response.body,:symbolize_keys => true)
|
81
|
+
return @tokens
|
82
|
+
end
|
83
|
+
|
84
|
+
def post(href, options={})
|
85
|
+
self.client.post href, options
|
86
|
+
end
|
87
|
+
|
88
|
+
# @param [String] enpoint The name of endpoint, for example: "people/getAll" or "groups/GetInfo"
|
89
|
+
# @param [Hash] option List of parametrs
|
90
|
+
# @result [Hash] Body of the API response
|
91
|
+
def call(endpoint, options={})
|
92
|
+
response = post endpoint, options
|
93
|
+
return response.body
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
configure
|
99
|
+
end
|
100
|
+
|
101
|
+
# require all the elvanto resources.
|
102
|
+
require_relative 'elvanto/resources'
|
metadata
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: elvanto-api
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Elvanto
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-05-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.7'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.7'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: faraday
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.8.6
|
48
|
+
- - <=
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: 0.9.0
|
51
|
+
type: :runtime
|
52
|
+
prerelease: false
|
53
|
+
version_requirements: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - '>='
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: 0.8.6
|
58
|
+
- - <=
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 0.9.0
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: faraday_middleware
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ~>
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: 0.9.0
|
68
|
+
type: :runtime
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ~>
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 0.9.0
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: addressable
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ~>
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: 2.3.5
|
82
|
+
type: :runtime
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ~>
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: 2.3.5
|
89
|
+
description: API wrapper for use in conjunction with an Elvanto account. This wrapper
|
90
|
+
can be used by developers to develop programs for their own churches using an API
|
91
|
+
Key, or to design integrations to share to other churches using OAuth authentication.
|
92
|
+
email:
|
93
|
+
- support@elvanto.com
|
94
|
+
executables: []
|
95
|
+
extensions: []
|
96
|
+
extra_rdoc_files: []
|
97
|
+
files:
|
98
|
+
- Gemfile
|
99
|
+
- Gemfile.lock
|
100
|
+
- LICENSE.txt
|
101
|
+
- README.md
|
102
|
+
- Rakefile
|
103
|
+
- elvanto.gemspec
|
104
|
+
- lib/elvanto.rb
|
105
|
+
- lib/elvanto/client.rb
|
106
|
+
- lib/elvanto/error.rb
|
107
|
+
- lib/elvanto/pager.rb
|
108
|
+
- lib/elvanto/resources.rb
|
109
|
+
- lib/elvanto/resources/resource.rb
|
110
|
+
- lib/elvanto/response/elvanto_exception_middleware.rb
|
111
|
+
- lib/elvanto/utils.rb
|
112
|
+
- lib/elvanto/version.rb
|
113
|
+
homepage: https://www.elvanto.com
|
114
|
+
licenses:
|
115
|
+
- MIT
|
116
|
+
metadata: {}
|
117
|
+
post_install_message:
|
118
|
+
rdoc_options: []
|
119
|
+
require_paths:
|
120
|
+
- lib
|
121
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - '>='
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
requirements: []
|
132
|
+
rubyforge_project:
|
133
|
+
rubygems_version: 2.4.7
|
134
|
+
signing_key:
|
135
|
+
specification_version: 4
|
136
|
+
summary: Ruby wrapper for Elvanto API
|
137
|
+
test_files: []
|