civic_aide 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.
@@ -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