easy311 0.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.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +22 -0
- data/README.md +39 -0
- data/Rakefile +1 -0
- data/easy311.gemspec +25 -0
- data/easy311.yml +4 -0
- data/lib/easy311.rb +11 -0
- data/lib/easy311/api_wrapper.rb +134 -0
- data/lib/easy311/attribute.rb +17 -0
- data/lib/easy311/factories.rb +65 -0
- data/lib/easy311/rails.rb +35 -0
- data/lib/easy311/request.rb +91 -0
- data/lib/easy311/response.rb +22 -0
- data/lib/easy311/service.rb +41 -0
- data/lib/easy311/version.rb +3 -0
- data/spec/easy311/api_wrapper_spec.rb +298 -0
- data/spec/easy311/json_helper.rb +51 -0
- data/spec/easy311/request_spec.rb +114 -0
- data/spec/spec_helper.rb +15 -0
- metadata +107 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Jimmy Bourassa
|
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,39 @@
|
|
1
|
+
# Easy311
|
2
|
+
|
3
|
+
Easy311 is a set of tools to ease the development of Open311 applications:
|
4
|
+
it provides a wrapper around the REST API and a close-to-compliant ActiveModel
|
5
|
+
`Request` object.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
gem 'easy311'
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install easy311
|
20
|
+
|
21
|
+
If you want to use `Easy311::Rails`, copy and edit easy311.yml
|
22
|
+
to `#{Rails.root}/config/easy311.yml`
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
The library can be used by itself without Rails, but we built a wrapper
|
27
|
+
for Rails that makes it easy to configure and load Open311's services.
|
28
|
+
|
29
|
+
No documentation official documentation has been written (yet) since
|
30
|
+
this library has originally been built during a Hackaton (see credits).
|
31
|
+
|
32
|
+
## Contributing
|
33
|
+
|
34
|
+
No contribution is too small: fork away and send pull requests!
|
35
|
+
|
36
|
+
## Credits
|
37
|
+
|
38
|
+
This gem has been extracted from 2013 [Iron Web](http://ironweb.org)
|
39
|
+
_Rouges_' team. Original code by @gelendir and @jbourassa.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/easy311.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'easy311/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "easy311"
|
8
|
+
gem.version = Easy311::VERSION
|
9
|
+
gem.authors = ["Jimmy Bourassa", "Gregory Eric Sanderson"]
|
10
|
+
gem.email = ["jimmy.bourassa@hooktstudios.com", "gzou2000@gmail.com"]
|
11
|
+
gem.description = %q{Set of tools to ease the development of Open311 applications}
|
12
|
+
gem.summary = %q{Easy311 is a set of tools to ease the development of
|
13
|
+
Open311 applications: it provides a wrapper around the
|
14
|
+
REST API and a close-to-compliant ActiveModel
|
15
|
+
`Request` object.}
|
16
|
+
gem.homepage = "https://github.com/jbourassa/easy311"
|
17
|
+
|
18
|
+
gem.files = `git ls-files`.split($/)
|
19
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
20
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
21
|
+
gem.require_paths = ["lib"]
|
22
|
+
|
23
|
+
gem.add_dependency('active_attr')
|
24
|
+
gem.add_dependency('rest-client')
|
25
|
+
end
|
data/easy311.yml
ADDED
data/lib/easy311.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require "active_attr"
|
2
|
+
require "easy311/version"
|
3
|
+
require "easy311/request"
|
4
|
+
require "easy311/api_wrapper"
|
5
|
+
require "easy311/service"
|
6
|
+
require "easy311/attribute"
|
7
|
+
require "easy311/response"
|
8
|
+
require "easy311/rails" if defined?(Rails)
|
9
|
+
|
10
|
+
module Easy311
|
11
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'rest-client'
|
2
|
+
require 'json'
|
3
|
+
require 'easy311/service'
|
4
|
+
require 'easy311/attribute'
|
5
|
+
require 'logger'
|
6
|
+
|
7
|
+
module Easy311
|
8
|
+
|
9
|
+
class ApiWrapper
|
10
|
+
|
11
|
+
attr_accessor :logger
|
12
|
+
|
13
|
+
def self.from_url(url, api_key, jurisdiction_id=nil)
|
14
|
+
resource = RestClient::Resource.new(url)
|
15
|
+
return self.new(resource, api_key, jurisdiction_id)
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(resource, api_key, jurisdiction_id=nil)
|
19
|
+
@resource = resource
|
20
|
+
@api_key = api_key
|
21
|
+
@jurisdiction_id = jurisdiction_id
|
22
|
+
end
|
23
|
+
|
24
|
+
def log
|
25
|
+
@logger ||= Easy311.logger
|
26
|
+
end
|
27
|
+
|
28
|
+
def all_services
|
29
|
+
log.info "querying list of services"
|
30
|
+
response = query_service('/services.json')
|
31
|
+
return [] if response.empty?
|
32
|
+
|
33
|
+
raw_services = JSON.parse(response)
|
34
|
+
services = raw_services.map { |r| Service.new(r) }
|
35
|
+
|
36
|
+
return services
|
37
|
+
end
|
38
|
+
|
39
|
+
def group_names
|
40
|
+
all_services.map { |s| s.group }.uniq
|
41
|
+
end
|
42
|
+
|
43
|
+
def groups
|
44
|
+
all_services.group_by { |s| s.group }
|
45
|
+
end
|
46
|
+
|
47
|
+
def attrs_from_code(code)
|
48
|
+
log.info "querying attributes for service #{code}"
|
49
|
+
url = "/services/#{code}.json"
|
50
|
+
response = query_service(url)
|
51
|
+
return [] if response.empty?
|
52
|
+
|
53
|
+
json_data = JSON.parse(response)
|
54
|
+
raw_attrs = json_data['attributes']
|
55
|
+
return [] if raw_attrs.empty?
|
56
|
+
|
57
|
+
attrs = raw_attrs.map { |a| Attribute.new(a) }
|
58
|
+
|
59
|
+
return attrs
|
60
|
+
end
|
61
|
+
|
62
|
+
def services_with_attrs
|
63
|
+
log.info "querying all services and attributes"
|
64
|
+
services = all_services.map do |service|
|
65
|
+
service.attrs = attrs_from_code(service.code)
|
66
|
+
service
|
67
|
+
end
|
68
|
+
return services
|
69
|
+
end
|
70
|
+
|
71
|
+
def query_service(path)
|
72
|
+
params = build_params
|
73
|
+
log.debug "GET #{path}"
|
74
|
+
response = @resource[path].get(params)
|
75
|
+
log.debug "RESPONSE #{response}"
|
76
|
+
return response.strip
|
77
|
+
end
|
78
|
+
|
79
|
+
def build_params
|
80
|
+
params = {}
|
81
|
+
params[:jurisdiction_id] = @jurisdiction_id unless @jurisdiction_id.nil?
|
82
|
+
return params
|
83
|
+
end
|
84
|
+
|
85
|
+
def send_request(request)
|
86
|
+
log.info "sending request #{request}"
|
87
|
+
|
88
|
+
params = request.to_post_params
|
89
|
+
params['api_key'] = @api_key
|
90
|
+
log.debug "params #{params}"
|
91
|
+
|
92
|
+
response = @resource['/requests.json'].post(params) do |http_response|
|
93
|
+
log.debug "RESPONSE #{http_response.code} #{http_response.body}"
|
94
|
+
if http_response.code.to_i >= 400
|
95
|
+
parse_error_response(http_response.body)
|
96
|
+
else
|
97
|
+
parse_post_response(http_response.body)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
log.info "response #{response}"
|
102
|
+
return response
|
103
|
+
end
|
104
|
+
|
105
|
+
def parse_post_response(body)
|
106
|
+
json_response = JSON.parse(body)
|
107
|
+
return Response.new(json_response.first)
|
108
|
+
end
|
109
|
+
|
110
|
+
def parse_error_response(body)
|
111
|
+
json_response = JSON.parse(body)
|
112
|
+
response = Response.new
|
113
|
+
response.error_message = json_response.map { |r| r['description'] }.join(",")
|
114
|
+
return response
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.logger=(logger)
|
120
|
+
@logger = logger
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.logger
|
124
|
+
@logger ||= rails_logger || default_logger
|
125
|
+
end
|
126
|
+
|
127
|
+
def self.rails_logger
|
128
|
+
defined?(::Rails) && ::Rails.respond_to?(:logger) && ::Rails.logger
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.default_logger
|
132
|
+
Logger.new(STDOUT)
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Easy311
|
2
|
+
|
3
|
+
class Attribute
|
4
|
+
include ActiveAttr::Model
|
5
|
+
|
6
|
+
attribute :code
|
7
|
+
attribute :datatype
|
8
|
+
attribute :description
|
9
|
+
attribute :datatype_description
|
10
|
+
attribute :order
|
11
|
+
attribute :required
|
12
|
+
attribute :values
|
13
|
+
attribute :variable
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
FactoryGirl.define do
|
3
|
+
factory :easy311_service, :class => 'Easy311::Service' do
|
4
|
+
service_code "1934303d-7f43-e111-85e1-005056a60032"
|
5
|
+
service_name "Collecte des encombrants - secteur résidentiel"
|
6
|
+
group "Ordures"
|
7
|
+
description "Description à venir"
|
8
|
+
metadata true
|
9
|
+
type "batch"
|
10
|
+
|
11
|
+
trait :with_optionnal do
|
12
|
+
keywords "Collecte,Encombrants,Gros déchets,Grosses poubelles,Grosses vidanges,Meubles,Monstres"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
factory :easy311_attribute, :class => 'Easy311::Attribute' do
|
17
|
+
code "7041ac51-ec75-e211-9483-005056a613ac"
|
18
|
+
datatype "text"
|
19
|
+
datatype_description "Pour disposer d`appareils contenant des halocarbures (congélateur réfrigérateur climatiseur etc.) veuillez communiquer avec votre bureau d'arrondissement."
|
20
|
+
description "Pour disposer d`appareils contenant des halocarbures (congélateur réfrigérateur climatiseur etc.) veuillez communiquer avec votre bureau d'arrondissement."
|
21
|
+
order 2
|
22
|
+
required false
|
23
|
+
values []
|
24
|
+
variable false
|
25
|
+
end
|
26
|
+
|
27
|
+
factory :easy311_request, :class => 'Easy311::Request' do
|
28
|
+
ignore do
|
29
|
+
service :nil
|
30
|
+
end
|
31
|
+
|
32
|
+
lat 12.34
|
33
|
+
long 56.78
|
34
|
+
|
35
|
+
initialize_with { new(FactoryGirl.build(:easy311_service)) }
|
36
|
+
end
|
37
|
+
|
38
|
+
factory :easy311_request_full, :class => 'Easy311Request' do
|
39
|
+
ignore do
|
40
|
+
service :nil
|
41
|
+
end
|
42
|
+
|
43
|
+
description "desc"
|
44
|
+
lat 12.34
|
45
|
+
long 56.78
|
46
|
+
email "email@test.com"
|
47
|
+
device_id 1
|
48
|
+
account_id 2
|
49
|
+
first_name "firstname"
|
50
|
+
last_name "lastname"
|
51
|
+
phone "55512345678"
|
52
|
+
description "description"
|
53
|
+
media_url "http://url"
|
54
|
+
|
55
|
+
initialize_with { new(FactoryGirl.build(:easy311_service)) }
|
56
|
+
end
|
57
|
+
|
58
|
+
factory :easy311_response, :class => 'Easy311::Response' do
|
59
|
+
token "53020772-c676-e211-9483-005056a613ac"
|
60
|
+
service_request_id nil
|
61
|
+
service_notice nil
|
62
|
+
account_id nil
|
63
|
+
error_message nil
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Easy311
|
2
|
+
module Rails
|
3
|
+
|
4
|
+
CACHE_KEY = 'easy311_services'
|
5
|
+
|
6
|
+
def self.config_path
|
7
|
+
"#{::Rails.root}/config/easy311.yml"
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.load_config
|
11
|
+
YAML.load(ERB.new(IO.read(config_path)).result)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.load_all_services!
|
15
|
+
api_wrapper = self.api_wrapper
|
16
|
+
services = api_wrapper.services_with_attrs
|
17
|
+
|
18
|
+
::Rails.cache.write CACHE_KEY, services
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.all_services
|
22
|
+
::Rails.cache.read(CACHE_KEY) || load_all_services!
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.api_wrapper
|
26
|
+
api_config = load_config
|
27
|
+
url = api_config['url']
|
28
|
+
jurisdiction_id = api_config['jurisdiction_id']
|
29
|
+
api_key = api_config['apikey']
|
30
|
+
|
31
|
+
ApiWrapper.from_url(url, api_key, jurisdiction_id)
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
2
|
+
module Easy311
|
3
|
+
class Request
|
4
|
+
include ActiveAttr::Model
|
5
|
+
|
6
|
+
attribute :description
|
7
|
+
attribute :email
|
8
|
+
attribute :first_name
|
9
|
+
attribute :last_name
|
10
|
+
attribute :phone
|
11
|
+
attribute :password
|
12
|
+
attribute :lat
|
13
|
+
attribute :long
|
14
|
+
attribute :device_id
|
15
|
+
attribute :account_id
|
16
|
+
attribute :media_url
|
17
|
+
|
18
|
+
attr_reader :service
|
19
|
+
attr_accessor :attrs_values
|
20
|
+
|
21
|
+
POST_KEYS = [
|
22
|
+
:service_code,
|
23
|
+
:description,
|
24
|
+
:lat,
|
25
|
+
:long,
|
26
|
+
:email,
|
27
|
+
:device_id,
|
28
|
+
:account_id,
|
29
|
+
:first_name,
|
30
|
+
:last_name,
|
31
|
+
:phone,
|
32
|
+
:description,
|
33
|
+
:media_url,
|
34
|
+
]
|
35
|
+
|
36
|
+
# TODO : Move out of Easy311 to our app -> not required by the spec
|
37
|
+
validates :email, :presence => true
|
38
|
+
validate :validate_required_attrs
|
39
|
+
|
40
|
+
def initialize(service)
|
41
|
+
@attrs_values = ActiveSupport::HashWithIndifferentAccess.new
|
42
|
+
@service = service
|
43
|
+
end
|
44
|
+
|
45
|
+
def attributes
|
46
|
+
super.merge("attrs" => Hash[service.attrs.map { |k, v| [k.to_s, self.send(k)]}])
|
47
|
+
end
|
48
|
+
|
49
|
+
def method_missing(method, *args, &block)
|
50
|
+
attr_name = method.to_s.gsub(/=$/, '')
|
51
|
+
if service.attrs.has_key?(attr_name)
|
52
|
+
if method.to_s.match(/=$/)
|
53
|
+
@attrs_values[attr_name] = args.first
|
54
|
+
else
|
55
|
+
@attrs_values[attr_name]
|
56
|
+
end
|
57
|
+
else
|
58
|
+
super
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def respond_to?(method, include_private=false)
|
63
|
+
if service.attrs.has_key?(method.to_s.gsub(/=$/, ''))
|
64
|
+
true
|
65
|
+
else
|
66
|
+
super
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_post_params
|
71
|
+
params = attributes.select { |k,v| !v.nil? and POST_KEYS.include? k.to_sym }
|
72
|
+
params['service_code'] = @service.code if @service.code
|
73
|
+
|
74
|
+
attrs_values.each do |key, value|
|
75
|
+
param_key = "attribute[#{key}]"
|
76
|
+
params[param_key] = value
|
77
|
+
end
|
78
|
+
|
79
|
+
params
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
def validate_required_attrs
|
84
|
+
required_attrs = @service.attrs.select { |code, attr| attr.required }.keys
|
85
|
+
return if required_attrs.empty?
|
86
|
+
validator = ActiveModel::Validations::PresenceValidator.new(
|
87
|
+
attributes: required_attrs)
|
88
|
+
validator.validate(self)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Easy311
|
2
|
+
|
3
|
+
class Response
|
4
|
+
include ActiveAttr::Model
|
5
|
+
|
6
|
+
attribute :service_request_id
|
7
|
+
attribute :token
|
8
|
+
attribute :service_notice
|
9
|
+
attribute :account_id
|
10
|
+
attribute :error_message
|
11
|
+
|
12
|
+
def valid?
|
13
|
+
error_message.nil?
|
14
|
+
end
|
15
|
+
|
16
|
+
def invalid?
|
17
|
+
!valid?
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Easy311
|
2
|
+
|
3
|
+
class Service
|
4
|
+
include ActiveAttr::Model
|
5
|
+
|
6
|
+
attribute :name
|
7
|
+
attribute :code
|
8
|
+
attribute :group
|
9
|
+
attribute :description
|
10
|
+
attribute :metadata
|
11
|
+
attribute :type
|
12
|
+
attribute :attrs, :default => {}
|
13
|
+
|
14
|
+
def service_name=(value)
|
15
|
+
self.name = value
|
16
|
+
end
|
17
|
+
|
18
|
+
def service_code=(value)
|
19
|
+
self.code = value
|
20
|
+
end
|
21
|
+
|
22
|
+
def attrs=(values)
|
23
|
+
if values.is_a?(Array)
|
24
|
+
super Hash[values.map { |value| [value.code, value] }]
|
25
|
+
elsif values.is_a?(Hash)
|
26
|
+
super values
|
27
|
+
else
|
28
|
+
raise TypeError
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def ordered_attrs
|
33
|
+
Hash[attrs.sort_by { |k, v| v.code }]
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_param
|
37
|
+
code
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,298 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require 'spec_helper'
|
3
|
+
require 'easy311/json_helper.rb'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
class ResponseStub
|
7
|
+
|
8
|
+
def initialize(response)
|
9
|
+
@response = response
|
10
|
+
end
|
11
|
+
|
12
|
+
def get(params)
|
13
|
+
return @response
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
class GetStub
|
19
|
+
attr_accessor :get
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
@responses = {}
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_response(url, response)
|
26
|
+
@responses[url] = response
|
27
|
+
end
|
28
|
+
|
29
|
+
def [](key)
|
30
|
+
return ResponseStub.new(@responses[key])
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
class PostStub
|
36
|
+
|
37
|
+
def initialize(response)
|
38
|
+
@response = response
|
39
|
+
end
|
40
|
+
|
41
|
+
def [](key)
|
42
|
+
return self
|
43
|
+
end
|
44
|
+
|
45
|
+
def post(payload)
|
46
|
+
code = 200
|
47
|
+
@result = payload
|
48
|
+
resp = Struct.new(:code, :body).new(code, @response)
|
49
|
+
yield resp
|
50
|
+
end
|
51
|
+
|
52
|
+
def post_result
|
53
|
+
@result
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
class ErrorStub < PostStub
|
59
|
+
|
60
|
+
def post(payload)
|
61
|
+
code = 400
|
62
|
+
body = "[{\"code\":\"Forbidden\",\"description\":\"'api_key' : L'argument ne peut pas \xC3\xAAtre null ou vide.\"}]"
|
63
|
+
response = Struct.new(:code, :body).new(code, body)
|
64
|
+
yield response
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
api_key = "12345678-1234-A1B2-C3D4-AA1A111A1A1A"
|
70
|
+
|
71
|
+
describe Easy311::ApiWrapper do
|
72
|
+
|
73
|
+
it "returns an empty list when no services" do
|
74
|
+
resource = GetStub.new
|
75
|
+
resource.add_response('/services.json', "")
|
76
|
+
|
77
|
+
wrapper = Easy311::ApiWrapper.new(resource, api_key)
|
78
|
+
wrapper.all_services.should == []
|
79
|
+
end
|
80
|
+
|
81
|
+
it "returns once service" do
|
82
|
+
resource = GetStub.new()
|
83
|
+
service_json = [sample_service].to_json
|
84
|
+
resource.add_response('/services.json', service_json)
|
85
|
+
|
86
|
+
wrapper = Easy311::ApiWrapper.new(resource, api_key)
|
87
|
+
all_services = wrapper.all_services
|
88
|
+
|
89
|
+
all_services.length.should == 1
|
90
|
+
service = all_services.first
|
91
|
+
|
92
|
+
service.name.should == sample_service['service_name']
|
93
|
+
service.code.should == sample_service['service_code']
|
94
|
+
service.group.should == sample_service['group']
|
95
|
+
service.description.should == sample_service['description']
|
96
|
+
service.metadata.should == sample_service['metadata']
|
97
|
+
service.type.should == sample_service['type']
|
98
|
+
service.to_param.should == sample_service['service_code']
|
99
|
+
end
|
100
|
+
|
101
|
+
it "returns a service with minimal keys" do
|
102
|
+
resource = GetStub.new
|
103
|
+
service_json = [min_sample_service].to_json
|
104
|
+
resource.add_response('/services.json', service_json)
|
105
|
+
|
106
|
+
wrapper = Easy311::ApiWrapper.new(resource, api_key)
|
107
|
+
all_services = wrapper.all_services
|
108
|
+
|
109
|
+
all_services.length.should == 1
|
110
|
+
service = all_services[0]
|
111
|
+
|
112
|
+
service.name.should == sample_service['service_name']
|
113
|
+
service.code.should == sample_service['service_code']
|
114
|
+
service.group.should == sample_service['group']
|
115
|
+
service.description.should == sample_service['description']
|
116
|
+
service.metadata.should == sample_service['metadata']
|
117
|
+
service.type.should == sample_service['type']
|
118
|
+
service.to_param.should == sample_service['service_code']
|
119
|
+
end
|
120
|
+
|
121
|
+
it "returns the names of the groups" do
|
122
|
+
resource = GetStub.new
|
123
|
+
service_json = [min_sample_service].to_json
|
124
|
+
resource.add_response('/services.json', service_json)
|
125
|
+
|
126
|
+
wrapper = Easy311::ApiWrapper.new(resource, "1234")
|
127
|
+
|
128
|
+
wrapper.group_names.should == ["Ordures"]
|
129
|
+
end
|
130
|
+
|
131
|
+
it "returns a single group name with 2 services" do
|
132
|
+
resource = GetStub.new
|
133
|
+
service_json = [sample_service, sample_service].to_json
|
134
|
+
resource.add_response('/services.json', service_json)
|
135
|
+
|
136
|
+
wrapper = Easy311::ApiWrapper.new(resource, "1234")
|
137
|
+
|
138
|
+
result = ["Ordures"]
|
139
|
+
expect(wrapper.group_names).to eq(result)
|
140
|
+
end
|
141
|
+
|
142
|
+
it "returns a single group name with 2 services" do
|
143
|
+
resource = GetStub.new
|
144
|
+
second_service = sample_service.dup
|
145
|
+
second_service['group'] = 'Trottoirs'
|
146
|
+
service_json = [sample_service, second_service].to_json
|
147
|
+
resource.add_response('/services.json', service_json)
|
148
|
+
|
149
|
+
wrapper = Easy311::ApiWrapper.new(resource, api_key)
|
150
|
+
|
151
|
+
result = ["Ordures", "Trottoirs"]
|
152
|
+
wrapper.group_names.should == result
|
153
|
+
end
|
154
|
+
|
155
|
+
it "groups services together" do
|
156
|
+
resource = GetStub.new
|
157
|
+
second_service = sample_service.dup
|
158
|
+
second_service['group'] = 'Trottoirs'
|
159
|
+
service_json = [sample_service, second_service].to_json
|
160
|
+
resource.add_response('/services.json', service_json)
|
161
|
+
|
162
|
+
wrapper = Easy311::ApiWrapper.new(resource, api_key)
|
163
|
+
|
164
|
+
groups = wrapper.groups
|
165
|
+
groups.keys.should == ["Ordures", "Trottoirs"]
|
166
|
+
groups["Ordures"].should be_an_instance_of(Array)
|
167
|
+
groups["Ordures"][0].should be_an_instance_of(Easy311::Service)
|
168
|
+
end
|
169
|
+
|
170
|
+
it "returns no attrs with an empty string" do
|
171
|
+
resource = GetStub.new
|
172
|
+
attribute_json = ""
|
173
|
+
resource.add_response('/services/01234-1234-1234-1234.json', attribute_json)
|
174
|
+
|
175
|
+
code = "01234-1234-1234-1234"
|
176
|
+
wrapper = Easy311::ApiWrapper.new(resource, "1234")
|
177
|
+
attrs = wrapper.attrs_from_code(code)
|
178
|
+
|
179
|
+
attrs.should == []
|
180
|
+
end
|
181
|
+
|
182
|
+
it "returns one attribute" do
|
183
|
+
resource = GetStub.new
|
184
|
+
attribute_json = sample_attribute.to_json
|
185
|
+
resource.add_response('/services/01234-1234-1234-1234.json', attribute_json)
|
186
|
+
|
187
|
+
code = "01234-1234-1234-1234"
|
188
|
+
wrapper = Easy311::ApiWrapper.new(resource, "1234")
|
189
|
+
result = wrapper.attrs_from_code(code)
|
190
|
+
|
191
|
+
expect(result.length).to eq(1)
|
192
|
+
attribute = result[0]
|
193
|
+
|
194
|
+
expect(attribute.code).to eq("7041ac51-ec75-e211-9483-005056a613ac")
|
195
|
+
expect(attribute.datatype).to eq("text")
|
196
|
+
expect(attribute.description).to eq("Pour disposer d`appareils contenant des halocarbures (congélateur, réfrigérateur, climatiseur, etc.), veuillez communiquer avec votre bureau d'arrondissement.")
|
197
|
+
expect(attribute.datatype_description).to eq("Pour disposer d`appareils contenant des halocarbures (congélateur, réfrigérateur, climatiseur, etc.), veuillez communiquer avec votre bureau d'arrondissement.")
|
198
|
+
expect(attribute.order).to eq(2)
|
199
|
+
expect(attribute.required).to eq(false)
|
200
|
+
expect(attribute.values).to eq([])
|
201
|
+
expect(attribute.variable).to eq(false)
|
202
|
+
end
|
203
|
+
|
204
|
+
it "returns a service with an attribute" do
|
205
|
+
resource = GetStub.new
|
206
|
+
service_json = [sample_service].to_json
|
207
|
+
attribute_json = sample_attribute.to_json
|
208
|
+
resource.add_response('/services.json', [sample_service].to_json)
|
209
|
+
resource.add_response('/services/1934303d-7f43-e111-85e1-005056a60032.json', attribute_json)
|
210
|
+
|
211
|
+
wrapper = Easy311::ApiWrapper.new(resource, "1234")
|
212
|
+
services = wrapper.services_with_attrs
|
213
|
+
service = services[0]
|
214
|
+
|
215
|
+
expect(service.name).to eq("Collecte des encombrants - secteur résidentiel")
|
216
|
+
expect(service.code).to eq("1934303d-7f43-e111-85e1-005056a60032")
|
217
|
+
expect(service.group).to eq("Ordures")
|
218
|
+
expect(service.description).to eq("Description à venir")
|
219
|
+
expect(service.metadata).to eq(true)
|
220
|
+
expect(service.type).to eq("batch")
|
221
|
+
|
222
|
+
service.attrs.should be_an_instance_of(Hash)
|
223
|
+
attribute = service.attrs.values.first
|
224
|
+
|
225
|
+
expect(attribute.code).to eq("7041ac51-ec75-e211-9483-005056a613ac")
|
226
|
+
expect(attribute.datatype).to eq("text")
|
227
|
+
expect(attribute.description).to eq("Pour disposer d`appareils contenant des halocarbures (congélateur, réfrigérateur, climatiseur, etc.), veuillez communiquer avec votre bureau d'arrondissement.")
|
228
|
+
expect(attribute.datatype_description).to eq("Pour disposer d`appareils contenant des halocarbures (congélateur, réfrigérateur, climatiseur, etc.), veuillez communiquer avec votre bureau d'arrondissement.")
|
229
|
+
expect(attribute.order).to eq(2)
|
230
|
+
expect(attribute.required).to eq(false)
|
231
|
+
expect(attribute.values).to eq([])
|
232
|
+
expect(attribute.variable).to eq(false)
|
233
|
+
end
|
234
|
+
|
235
|
+
it "sends a request to the api" do
|
236
|
+
resource = PostStub.new([sample_response].to_json)
|
237
|
+
request = FactoryGirl.build(:easy311_request)
|
238
|
+
|
239
|
+
wrapper = Easy311::ApiWrapper.new(resource, api_key)
|
240
|
+
wrapper.send_request(request)
|
241
|
+
|
242
|
+
expected = {
|
243
|
+
'api_key' => api_key,
|
244
|
+
'service_code' => request.service.code,
|
245
|
+
'lat' => request.lat,
|
246
|
+
'long' => request.long,
|
247
|
+
}
|
248
|
+
|
249
|
+
resource.post_result.should == expected
|
250
|
+
end
|
251
|
+
|
252
|
+
it "sends a request with attributes to the api" do
|
253
|
+
resource = PostStub.new([sample_response].to_json)
|
254
|
+
request = FactoryGirl.build(:easy311_request)
|
255
|
+
request.attrs_values = {
|
256
|
+
'1234-1234-1234-1234' => '2345-2345-2345-2345',
|
257
|
+
'3456-3456-3456-3456' => '4567-4567-4567-4567'
|
258
|
+
}
|
259
|
+
|
260
|
+
wrapper = Easy311::ApiWrapper.new(resource, api_key)
|
261
|
+
wrapper.send_request(request)
|
262
|
+
|
263
|
+
expected = {
|
264
|
+
'api_key' => api_key,
|
265
|
+
'service_code' => request.service.code,
|
266
|
+
'lat' => request.lat,
|
267
|
+
'long' => request.long,
|
268
|
+
'attribute[1234-1234-1234-1234]' => '2345-2345-2345-2345',
|
269
|
+
'attribute[3456-3456-3456-3456]' => '4567-4567-4567-4567',
|
270
|
+
}
|
271
|
+
|
272
|
+
resource.post_result.should == expected
|
273
|
+
end
|
274
|
+
|
275
|
+
it "returns a response when sending a request" do
|
276
|
+
resource = PostStub.new([sample_response].to_json)
|
277
|
+
request = FactoryGirl.build(:easy311_request)
|
278
|
+
wrapper = Easy311::ApiWrapper.new(resource, api_key)
|
279
|
+
|
280
|
+
response = wrapper.send_request(request)
|
281
|
+
|
282
|
+
response.account_id.should be_nil
|
283
|
+
response.service_notice.should be_nil
|
284
|
+
response.service_request_id.should be_nil
|
285
|
+
response.token.should == "c5f0d6ad-59ca-44a4-86e8-a2d72708f54c"
|
286
|
+
end
|
287
|
+
|
288
|
+
it "raises an exception when api returns a 400 error" do
|
289
|
+
resource = ErrorStub.new([sample_response].to_json)
|
290
|
+
request = FactoryGirl.build(:easy311_request)
|
291
|
+
|
292
|
+
wrapper = Easy311::ApiWrapper.new(resource, api_key)
|
293
|
+
response = wrapper.send_request(request)
|
294
|
+
response.invalid?.should == true
|
295
|
+
response.error_message.should == "'api_key' : L'argument ne peut pas \xC3\xAAtre null ou vide."
|
296
|
+
end
|
297
|
+
|
298
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
def min_sample_service(opts={})
|
3
|
+
{
|
4
|
+
"service_code"=>"1934303d-7f43-e111-85e1-005056a60032",
|
5
|
+
"service_name"=>"Collecte des encombrants - secteur résidentiel",
|
6
|
+
"group"=>"Ordures",
|
7
|
+
"description"=>"Description à venir",
|
8
|
+
"metadata"=>true,
|
9
|
+
"type"=>"batch"
|
10
|
+
}.merge(opts)
|
11
|
+
end
|
12
|
+
|
13
|
+
def sample_service(opts={})
|
14
|
+
{
|
15
|
+
"description"=>"Description à venir",
|
16
|
+
"group"=>"Ordures",
|
17
|
+
"keywords"=> "Collecte,Encombrants,Gros déchets,Grosses poubelles,Grosses vidanges,Meubles,Monstres",
|
18
|
+
"metadata"=>true,
|
19
|
+
"service_code"=>"1934303d-7f43-e111-85e1-005056a60032",
|
20
|
+
"service_name"=>"Collecte des encombrants - secteur résidentiel",
|
21
|
+
"type"=>"batch"
|
22
|
+
}.merge(opts)
|
23
|
+
end
|
24
|
+
|
25
|
+
def sample_attribute
|
26
|
+
{
|
27
|
+
"attributes"=> [one_attribute]
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
def one_attribute(opts={})
|
32
|
+
{
|
33
|
+
"code"=>"7041ac51-ec75-e211-9483-005056a613ac",
|
34
|
+
"datatype"=>"text",
|
35
|
+
"datatype_description"=> "Pour disposer d`appareils contenant des halocarbures (congélateur, réfrigérateur, climatiseur, etc.), veuillez communiquer avec votre bureau d'arrondissement.",
|
36
|
+
"description"=> "Pour disposer d`appareils contenant des halocarbures (congélateur, réfrigérateur, climatiseur, etc.), veuillez communiquer avec votre bureau d'arrondissement.",
|
37
|
+
"order"=>2,
|
38
|
+
"required"=>false,
|
39
|
+
"values"=>[],
|
40
|
+
"variable"=>false
|
41
|
+
}.merge(opts)
|
42
|
+
end
|
43
|
+
|
44
|
+
def sample_response(opts={})
|
45
|
+
{
|
46
|
+
"account_id" => nil,
|
47
|
+
"service_notice" => nil,
|
48
|
+
"service_request_id" => nil,
|
49
|
+
"token" => "c5f0d6ad-59ca-44a4-86e8-a2d72708f54c"
|
50
|
+
}.merge(opts)
|
51
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
describe Easy311::Request do
|
2
|
+
|
3
|
+
describe "responds to method missing" do
|
4
|
+
|
5
|
+
subject do
|
6
|
+
service = FactoryGirl.build(:easy311_service, attrs: [
|
7
|
+
FactoryGirl.build(:easy311_attribute, :code => "some_attribute")])
|
8
|
+
Easy311::Request.new(service)
|
9
|
+
end
|
10
|
+
it { should respond_to(:some_attribute) }
|
11
|
+
it { should respond_to(:some_attribute=) }
|
12
|
+
it { should_not respond_to(:invalid_attribute) }
|
13
|
+
it { should_not respond_to(:invalid_attribute=) }
|
14
|
+
it "writes Easy311 attribute values to ActiveAttr's" do
|
15
|
+
subject.some_attribute = 'Value 123'
|
16
|
+
subject.some_attribute.should == 'Value 123'
|
17
|
+
end
|
18
|
+
|
19
|
+
it "merges ActiveAttr and Easy311 fields when calling #attributes" do
|
20
|
+
subject.some_attribute = "some_value"
|
21
|
+
subject.attrs_values.should have_key("some_attribute")
|
22
|
+
end
|
23
|
+
|
24
|
+
it "has the same value in attributes" do
|
25
|
+
subject.some_attribute = "some_value"
|
26
|
+
subject.attributes["attrs"]["some_attribute"].should == "some_value"
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "generates attributes to send to api_wrapper" do
|
32
|
+
|
33
|
+
it "converts lat and long" do
|
34
|
+
service = FactoryGirl.build(:easy311_service)
|
35
|
+
service.code = "1234"
|
36
|
+
|
37
|
+
request = Easy311::Request.new(service)
|
38
|
+
request.lat = 12.23
|
39
|
+
request.long = 23.34
|
40
|
+
|
41
|
+
expected = {
|
42
|
+
'service_code' => "1234",
|
43
|
+
'lat' => 12.23,
|
44
|
+
'long' => 23.34,
|
45
|
+
}
|
46
|
+
|
47
|
+
request.to_post_params.should == expected
|
48
|
+
end
|
49
|
+
|
50
|
+
it "converts all attributes to send to api_wrapper" do
|
51
|
+
service = FactoryGirl.build(:easy311_service)
|
52
|
+
service.code = "1234"
|
53
|
+
|
54
|
+
params = {
|
55
|
+
'service_code' => service.code,
|
56
|
+
'description' => "desc",
|
57
|
+
'lat' => 12.34,
|
58
|
+
'long' => 56.78,
|
59
|
+
'email' => "email@test.com",
|
60
|
+
'device_id' => 1,
|
61
|
+
'account_id' => 2,
|
62
|
+
'first_name' => "firstname",
|
63
|
+
'last_name' => "lastname",
|
64
|
+
'phone' => "55512345678",
|
65
|
+
'description' => "description",
|
66
|
+
'media_url' => "http://url"
|
67
|
+
}
|
68
|
+
|
69
|
+
request = Easy311::Request.new(service)
|
70
|
+
params.except('service_code').each do |key, value|
|
71
|
+
request.send("#{key}=", value)
|
72
|
+
end
|
73
|
+
|
74
|
+
request.to_post_params.should == params
|
75
|
+
end
|
76
|
+
|
77
|
+
it "converts question values to send to api_wrapper" do
|
78
|
+
service = FactoryGirl.build(:easy311_service)
|
79
|
+
service.code = "1234"
|
80
|
+
|
81
|
+
request = Easy311::Request.new(service)
|
82
|
+
request.lat = 12.34
|
83
|
+
request.long = 23.45
|
84
|
+
request.attrs_values['value1'] = "param1"
|
85
|
+
|
86
|
+
expected = {
|
87
|
+
'service_code' => "1234",
|
88
|
+
'lat' => 12.34,
|
89
|
+
'long' => 23.45,
|
90
|
+
'attribute[value1]' => "param1",
|
91
|
+
}
|
92
|
+
|
93
|
+
request.to_post_params.should == expected
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
describe "validations" do
|
99
|
+
it "responds to valid?" do
|
100
|
+
FactoryGirl.build(:easy311_service).should respond_to(:valid?)
|
101
|
+
end
|
102
|
+
|
103
|
+
it "validates required attributes" do
|
104
|
+
required_attr = FactoryGirl.build(:easy311_attribute, required: true)
|
105
|
+
service = FactoryGirl.build(:easy311_service, :attrs => [required_attr])
|
106
|
+
|
107
|
+
request = Easy311::Request.new(service)
|
108
|
+
request.valid?.should be_false
|
109
|
+
request.errors.should have_key(required_attr.code.to_sym)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require "easy311"
|
2
|
+
require "factory_girl"
|
3
|
+
require "easy311/factories"
|
4
|
+
|
5
|
+
Easy311.logger = Logger.new('/dev/null')
|
6
|
+
|
7
|
+
RSpec.configure do |config|
|
8
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
9
|
+
|
10
|
+
# Run specs in random order to surface order dependencies. If you find an
|
11
|
+
# order dependency and want to debug it, you can fix the order by providing
|
12
|
+
# the seed, which is printed after each run.
|
13
|
+
# --seed 1234
|
14
|
+
config.order = 'random'
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: easy311
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.1
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jimmy Bourassa
|
9
|
+
- Gregory Eric Sanderson
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2013-02-23 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
version_requirements: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ! '>='
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '0'
|
21
|
+
none: false
|
22
|
+
name: active_attr
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
requirement: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
none: false
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
version_requirements: !ruby/object:Gem::Requirement
|
33
|
+
requirements:
|
34
|
+
- - ! '>='
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '0'
|
37
|
+
none: false
|
38
|
+
name: rest-client
|
39
|
+
type: :runtime
|
40
|
+
prerelease: false
|
41
|
+
requirement: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
none: false
|
47
|
+
description: Set of tools to ease the development of Open311 applications
|
48
|
+
email:
|
49
|
+
- jimmy.bourassa@hooktstudios.com
|
50
|
+
- gzou2000@gmail.com
|
51
|
+
executables: []
|
52
|
+
extensions: []
|
53
|
+
extra_rdoc_files: []
|
54
|
+
files:
|
55
|
+
- .gitignore
|
56
|
+
- .rspec
|
57
|
+
- Gemfile
|
58
|
+
- LICENSE.txt
|
59
|
+
- README.md
|
60
|
+
- Rakefile
|
61
|
+
- easy311.gemspec
|
62
|
+
- easy311.yml
|
63
|
+
- lib/easy311.rb
|
64
|
+
- lib/easy311/api_wrapper.rb
|
65
|
+
- lib/easy311/attribute.rb
|
66
|
+
- lib/easy311/factories.rb
|
67
|
+
- lib/easy311/rails.rb
|
68
|
+
- lib/easy311/request.rb
|
69
|
+
- lib/easy311/response.rb
|
70
|
+
- lib/easy311/service.rb
|
71
|
+
- lib/easy311/version.rb
|
72
|
+
- spec/easy311/api_wrapper_spec.rb
|
73
|
+
- spec/easy311/json_helper.rb
|
74
|
+
- spec/easy311/request_spec.rb
|
75
|
+
- spec/spec_helper.rb
|
76
|
+
homepage: https://github.com/jbourassa/easy311
|
77
|
+
licenses: []
|
78
|
+
post_install_message:
|
79
|
+
rdoc_options: []
|
80
|
+
require_paths:
|
81
|
+
- lib
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - ! '>='
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
none: false
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ! '>='
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
none: false
|
94
|
+
requirements: []
|
95
|
+
rubyforge_project:
|
96
|
+
rubygems_version: 1.8.23
|
97
|
+
signing_key:
|
98
|
+
specification_version: 3
|
99
|
+
summary: ! 'Easy311 is a set of tools to ease the development of Open311 applications:
|
100
|
+
it provides a wrapper around the REST API and a close-to-compliant ActiveModel `Request`
|
101
|
+
object.'
|
102
|
+
test_files:
|
103
|
+
- spec/easy311/api_wrapper_spec.rb
|
104
|
+
- spec/easy311/json_helper.rb
|
105
|
+
- spec/easy311/request_spec.rb
|
106
|
+
- spec/spec_helper.rb
|
107
|
+
has_rdoc:
|