civic_aide 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new
5
+
6
+ task :default => :spec
7
+ task :test => :spec
@@ -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
@@ -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,9 @@
1
+ class String
2
+ def underscore
3
+ self.gsub(/::/, '/').
4
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
5
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
6
+ tr("-", "_").
7
+ downcase
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module CivicAide
2
+ VERSION = "0.0.1"
3
+ 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