civic_aide 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.travis.yml +8 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +454 -0
- data/Rakefile +7 -0
- data/civic_aide.gemspec +29 -0
- data/lib/civic_aide.rb +34 -0
- data/lib/civic_aide/client.rb +71 -0
- data/lib/civic_aide/elections.rb +22 -0
- data/lib/civic_aide/errors.rb +56 -0
- data/lib/civic_aide/hash.rb +36 -0
- data/lib/civic_aide/representatives.rb +16 -0
- data/lib/civic_aide/string.rb +9 -0
- data/lib/civic_aide/version.rb +3 -0
- data/spec/civic_aide/client_spec.rb +62 -0
- data/spec/civic_aide/elections_spec.rb +130 -0
- data/spec/civic_aide/errors_spec.rb +21 -0
- data/spec/civic_aide/hash_spec.rb +61 -0
- data/spec/civic_aide/representatives_spec.rb +137 -0
- data/spec/civic_aide/string_spec.rb +16 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/vcr/elections/all.yml +63 -0
- data/spec/vcr/elections/single.yml +83 -0
- data/spec/vcr/representatives/single.yml +106 -0
- metadata +178 -0
data/Rakefile
ADDED
data/civic_aide.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'civic_aide/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "civic_aide"
|
8
|
+
spec.version = CivicAide::VERSION
|
9
|
+
spec.authors = ["Tyler Pearson"]
|
10
|
+
spec.email = ["ty.pearson@gmail.com"]
|
11
|
+
spec.summary = %q{A Ruby wrapper for the Google Civic Information API}
|
12
|
+
spec.description = spec.summary
|
13
|
+
spec.homepage = "https://github.com/tylerpearson/civic_aide"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
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.3"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.1.1"
|
23
|
+
spec.add_development_dependency "rspec", "~> 2.14.1"
|
24
|
+
spec.add_development_dependency "vcr", '~> 2.5.0'
|
25
|
+
spec.add_development_dependency "webmock", '1.13.0'
|
26
|
+
|
27
|
+
spec.add_runtime_dependency "httparty"
|
28
|
+
spec.add_runtime_dependency "hashie"
|
29
|
+
end
|
data/lib/civic_aide.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'hashie'
|
3
|
+
require 'civic_aide/version'
|
4
|
+
require 'civic_aide/client'
|
5
|
+
require 'civic_aide/hash'
|
6
|
+
require 'civic_aide/string'
|
7
|
+
require 'civic_aide/elections'
|
8
|
+
require 'civic_aide/representatives'
|
9
|
+
require 'civic_aide/errors'
|
10
|
+
|
11
|
+
module CivicAide
|
12
|
+
class << self
|
13
|
+
extend Forwardable
|
14
|
+
|
15
|
+
def api_key
|
16
|
+
raise APIKeyNotSet if @api_key.nil?
|
17
|
+
@api_key
|
18
|
+
end
|
19
|
+
|
20
|
+
def api_key=(api_key)
|
21
|
+
@api_key = api_key
|
22
|
+
end
|
23
|
+
|
24
|
+
delegate [
|
25
|
+
:elections,
|
26
|
+
:representatives
|
27
|
+
] => :client
|
28
|
+
|
29
|
+
def client
|
30
|
+
@client = CivicAide::Client.new(@api_key)
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module CivicAide
|
5
|
+
class Client
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
include HTTParty
|
9
|
+
|
10
|
+
API_ENDPOINT = 'https://www.googleapis.com/civicinfo/'
|
11
|
+
API_VERSION = 'us_v1'
|
12
|
+
|
13
|
+
base_uri "#{API_ENDPOINT}#{API_VERSION}"
|
14
|
+
headers "Content-Type" => "application/json"
|
15
|
+
headers "User-Agent" => "CivicAide Ruby gem v#{CivicAide::VERSION}".freeze
|
16
|
+
|
17
|
+
attr_reader :api_key
|
18
|
+
|
19
|
+
def initialize(api_key=nil)
|
20
|
+
@api_key = api_key
|
21
|
+
@api_key ||= CivicAide.api_key
|
22
|
+
end
|
23
|
+
|
24
|
+
def get(url, query={})
|
25
|
+
response = self.class.get(url, :query => query.merge(self.default_query))
|
26
|
+
format_response(response.body)
|
27
|
+
end
|
28
|
+
|
29
|
+
def post(url, query={}, body={})
|
30
|
+
response = self.class.post(url,
|
31
|
+
:query => query.merge(self.default_query),
|
32
|
+
:body => body.to_json
|
33
|
+
)
|
34
|
+
check_response_status(response['status'])
|
35
|
+
format_response(response.body)
|
36
|
+
end
|
37
|
+
|
38
|
+
def elections(election_id=nil)
|
39
|
+
CivicAide::Elections.new(self, election_id)
|
40
|
+
end
|
41
|
+
|
42
|
+
def representatives
|
43
|
+
CivicAide::Representatives.new(self)
|
44
|
+
end
|
45
|
+
|
46
|
+
protected
|
47
|
+
|
48
|
+
def default_query
|
49
|
+
{:key => @api_key, :prettyPrint => false}
|
50
|
+
end
|
51
|
+
|
52
|
+
def format_response(body)
|
53
|
+
body = JSON.parse(body)
|
54
|
+
body.change_zip! # to prevent Array#zip clashing when rubifying the keys
|
55
|
+
body.rubyify_keys!
|
56
|
+
Hashie::Mash.new(body)
|
57
|
+
end
|
58
|
+
|
59
|
+
def check_response_status(code)
|
60
|
+
unless code.downcase == "success"
|
61
|
+
error_type = classify_error(code)
|
62
|
+
raise error_type
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def classify_error(code)
|
67
|
+
code.slice(0,1).capitalize + code.slice(1..-1)
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module CivicAide
|
2
|
+
class Elections
|
3
|
+
attr_reader :client, :election_id
|
4
|
+
|
5
|
+
def initialize(client, election_id=nil)
|
6
|
+
@client = client
|
7
|
+
@election_id = election_id
|
8
|
+
end
|
9
|
+
|
10
|
+
def all
|
11
|
+
response = client.get('/elections')
|
12
|
+
response.except!(:kind)
|
13
|
+
end
|
14
|
+
|
15
|
+
def at(address)
|
16
|
+
raise ElectionIdMissing, "Missing a required election id" if @election_id.nil?
|
17
|
+
response = client.post("/voterinfo/#{election_id}/lookup", {officialOnly: false}, {:address => address})
|
18
|
+
response.except!(:kind, :status)
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
class APIError < StandardError; end
|
2
|
+
|
3
|
+
class APIKeyNotSet < StandardError
|
4
|
+
def initialize(msg = "Missing a required Google API key.")
|
5
|
+
super
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class ElectionIdMissing < StandardError
|
10
|
+
def initialize(msg = "Missing a required election id.")
|
11
|
+
super
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class NoStreetSegmentFound < APIError
|
16
|
+
def initialize(msg = "The API currently has no information about what electoral precinct and/or district this address belongs to. It may be that we are still sourcing/processing new data, or that there are no voters who have registered to vote at this address.")
|
17
|
+
super
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class AddressUnparseable < APIError
|
22
|
+
def initialize(msg = "The requested address is not formatted correctly or cannot be geocoded (i.e. the Google Maps API does not know anything about this address).")
|
23
|
+
super
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class NoAddressParameter < APIError
|
28
|
+
def initialize(msg = "No address was provided.")
|
29
|
+
super
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class MultipleStreetSegmentsFound < APIError
|
34
|
+
def initialize(msg = "The API cannot find information for the specified address, but it has information about nearby addresses. The user should contact their election official for more information.")
|
35
|
+
super
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class ElectionOver < APIError
|
40
|
+
def initialize(msg = "The requested election is over. API results for the election are no longer available. Make an electionQuery to find an id for an upcoming election.")
|
41
|
+
super
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class ElectionUnknown < APIError
|
46
|
+
def initialize(msg = "The requested election id is invalid. Make an electionQuery to find a valid id.")
|
47
|
+
super
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class InternalLookupFailure < APIError
|
52
|
+
def initialize(msg = "An unspecified error occurred processing the request.")
|
53
|
+
super
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class Hash
|
2
|
+
|
3
|
+
def rubyify_keys!
|
4
|
+
keys.each do |k|
|
5
|
+
val = self[k]
|
6
|
+
# ignore Open Civic Data identifiers
|
7
|
+
unless k[0..3] == "ocd-"
|
8
|
+
delete(k)
|
9
|
+
new_key = k.to_s.underscore
|
10
|
+
self[new_key] = val
|
11
|
+
end
|
12
|
+
val.rubyify_keys! if val.is_a?(Hash)
|
13
|
+
val.each{|p| p.rubyify_keys! if p.is_a?(Hash)} if val.is_a?(Array)
|
14
|
+
end
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
def except(*keys)
|
19
|
+
dup.except!(*keys)
|
20
|
+
end
|
21
|
+
|
22
|
+
def except!(*keys)
|
23
|
+
keys.each { |key| delete(key) }
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def change_zip!
|
28
|
+
keys.each do |k|
|
29
|
+
self["zipCode"] = self.delete "zip" if k == "zip"
|
30
|
+
self[k].change_zip! if self[k].is_a? Hash
|
31
|
+
self[k].each{|p| p.change_zip! if p.is_a?(Hash)} if self[k].is_a?(Array)
|
32
|
+
end
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module CivicAide
|
2
|
+
class Representatives
|
3
|
+
attr_reader :client, :include_offices
|
4
|
+
|
5
|
+
def initialize(client)
|
6
|
+
@client = client
|
7
|
+
@include_offices = true
|
8
|
+
end
|
9
|
+
|
10
|
+
def at(address)
|
11
|
+
response = client.post("/representatives/lookup", {includeOffices: @include_offices}, {:address => address})
|
12
|
+
response.except!(:kind, :status)
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe CivicAide::Client do
|
4
|
+
|
5
|
+
before do
|
6
|
+
@client = CivicAide::Client.new("AIzaSyDWJSisG_4Azd6nVJTU5gdKPiKKTCovupY")
|
7
|
+
end
|
8
|
+
|
9
|
+
describe '.new' do
|
10
|
+
it 'raises an error with no API key' do
|
11
|
+
expect{ CivicAide::Client.new }.to raise_error(APIKeyNotSet)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "doesn't raise an error with class API key set" do
|
15
|
+
CivicAide.api_key = "AIzaSyDWJSisG_4Azd6nVJTU5gdKPiKKTCovupY"
|
16
|
+
expect{ CivicAide::Client.new }.to_not raise_error
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "#new" do
|
21
|
+
it "takes one parameter and returns a Client object" do
|
22
|
+
@client.should be_an_instance_of CivicAide::Client
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe 'configuration' do
|
27
|
+
it 'should have correct API endpoint' do
|
28
|
+
expect(CivicAide::Client::API_ENDPOINT).to eq('https://www.googleapis.com/civicinfo/')
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'should have the correct API version' do
|
32
|
+
expect(CivicAide::Client::API_VERSION).to eq('us_v1')
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should include HTTParty' do
|
36
|
+
@client.extend(HTTParty)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should have the correct base_uri' do
|
40
|
+
expect(CivicAide::Client.base_uri).to eq('https://www.googleapis.com/civicinfo/us_v1')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe '#default_query' do
|
45
|
+
it 'should have the api key' do
|
46
|
+
@client.send(:default_query).should == {:key => @client.api_key, :prettyPrint => false}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe "#elections" do
|
51
|
+
it "should be the right class" do
|
52
|
+
expect(@client.elections).to be_an_instance_of CivicAide::Elections
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "#representatives" do
|
57
|
+
it "should be the right class" do
|
58
|
+
expect(@client.representatives).to be_an_instance_of CivicAide::Representatives
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe CivicAide::Elections do
|
4
|
+
|
5
|
+
before do
|
6
|
+
@client = CivicAide::Client.new("AIzaSyDWJSisG_4Azd6nVJTU5gdKPiKKTCovupY")
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "get all elections" do
|
10
|
+
|
11
|
+
let(:info) { @client.elections.all }
|
12
|
+
|
13
|
+
before do
|
14
|
+
VCR.insert_cassette 'elections/all', :record => :new_episodes
|
15
|
+
@election = info.elections.first
|
16
|
+
end
|
17
|
+
|
18
|
+
after do
|
19
|
+
VCR.eject_cassette
|
20
|
+
end
|
21
|
+
|
22
|
+
it "must parse the response from JSON to Hash" do
|
23
|
+
expect(info).to be_a Hash
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should have elections" do
|
27
|
+
expect(info).to respond_to(:elections)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should have multiple elections" do
|
31
|
+
info.elections.should_not be_empty
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should have an id" do
|
35
|
+
expect(@election.id).to eq("2000")
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should have a name" do
|
39
|
+
expect(@election.name).to eq("VIP Test Election")
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should have an election day" do
|
43
|
+
expect(@election.election_day).to eq("2015-06-06")
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "get info for specific election" do
|
49
|
+
|
50
|
+
let(:info) { @client.elections(4015).at('4910 Willet Drive, Annandale, VA 22003') }
|
51
|
+
|
52
|
+
before do
|
53
|
+
VCR.insert_cassette 'elections/single', :record => :new_episodes
|
54
|
+
end
|
55
|
+
|
56
|
+
after do
|
57
|
+
VCR.eject_cassette
|
58
|
+
end
|
59
|
+
|
60
|
+
it "must parse the response from JSON to Hash" do
|
61
|
+
expect(info).to be_a Hash
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should have an election" do
|
65
|
+
expect(info.election).to be_a Hash
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should have an election id" do
|
69
|
+
expect(info.election.id).to eq("4015")
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should have an election name" do
|
73
|
+
expect(info.election.name).to eq("VA State Election")
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should have an election day" do
|
77
|
+
expect(info.election.election_day).to eq("2013-11-05")
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should have normalized input" do
|
81
|
+
expect(info).to respond_to(:normalized_input)
|
82
|
+
end
|
83
|
+
|
84
|
+
it "should have normalized input hash" do
|
85
|
+
expect(info.normalized_input).to be_a Hash
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should have normalized input line 1" do
|
89
|
+
expect(info.normalized_input.line1).to eq("4910 willet dr")
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should have normalized input city" do
|
93
|
+
expect(info.normalized_input.city).to eq("annandale")
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should have normalized input state" do
|
97
|
+
expect(info.normalized_input.state).to eq("VA")
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should have normalized input zip" do
|
101
|
+
expect(info.normalized_input.zip_code).to eq("22003")
|
102
|
+
end
|
103
|
+
|
104
|
+
it "should have contests" do
|
105
|
+
expect(info).to respond_to(:contests)
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should have contests as array" do
|
109
|
+
expect(info.contests).to be_a Array
|
110
|
+
end
|
111
|
+
|
112
|
+
it "should have candidates" do
|
113
|
+
expect(info.contests[0]).to respond_to(:candidates)
|
114
|
+
end
|
115
|
+
|
116
|
+
it "should have candidates array" do
|
117
|
+
expect(info.contests[0].candidates).to be_a Array
|
118
|
+
end
|
119
|
+
|
120
|
+
it "should have sources" do
|
121
|
+
expect(info.contests[0]).to respond_to(:sources)
|
122
|
+
end
|
123
|
+
|
124
|
+
it "should have sources array" do
|
125
|
+
expect(info.contests[0].sources).to be_a Array
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|