get_your_rep 0.1.9 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,87 +1,69 @@
1
+ # frozen_string_literal: true
1
2
  module GetYourRep
2
-
3
- # The Delegation class inherits from Array and describes methods that can be called on a Delegation for ease of display and database query.
4
- class Delegation < Array
5
-
6
- # Overloads the << operator so the receiving object is not overwritten as an Array, and there are no nested Delegations.
7
- def <<(value)
8
-
9
- if value.is_a?(Delegation) || value.is_a?(Array)
10
- self.replace(self + value)
11
- else
12
- super
13
- end
14
-
15
- end
16
-
17
- # Overloads the + operator to return a new Delegation instead of an Array.
18
- def +(value)
19
- super.to_del
20
- end
21
-
22
- # Overloads #reverse to return a new Delegation instead of an Array.
23
- def reverse
24
- self.replace(super)
25
- end
26
-
27
- # Collects the first names of every rep in the Delegation.
28
- def first_names
29
- self.map { |rep| rep.first_name }
30
- end
31
-
32
- # Collects the last names of every rep in the Delegation.
33
- def last_names
34
- self.map { |rep| rep.last_name }
35
- end
36
-
37
- # Maps all rep business cards.
38
- def business_cards
39
- self.map { |rep| rep.business_card }
3
+ # The Delegation class holds an array of Representative objects. It can enumerate on its reps.
4
+ class Delegation
5
+ include Enumerable
6
+ include GetYourRep::Errors
7
+
8
+ # Set the reps attribute, which will hold an array of Representative objects, to an empty array by default.
9
+ def initialize
10
+ @reps = []
40
11
  end
41
12
 
42
- # Get the [1] index.
43
- def second
44
- self[1]
13
+ # Defines the enumerator methods on the :reps attribute.
14
+ def each(&block)
15
+ @reps.each(&block)
45
16
  end
46
17
 
47
- # Get the [2] index.
48
- def third
49
- self[2]
18
+ # Merge reps with another Delegation. This will add the reps of other to the reps of self,
19
+ # and leave other unchanged. Only accepts a Delegation as an argument.
20
+ def merge(other)
21
+ if other.is_a?(self.class)
22
+ other.reps.each { |rep| add_rep(rep) }
23
+ self
24
+ else
25
+ not_a_del_error
26
+ end
50
27
  end
51
28
 
52
- # Get the [3] index.
53
- def fourth
54
- self[3]
29
+ # Returns a frozen duplicate of the reps array.
30
+ def reps
31
+ @reps.dup.freeze
55
32
  end
56
33
 
57
- # Get the [4] index.
58
- def fifth
59
- self[4]
34
+ # Empties the reps array.
35
+ def clear_reps
36
+ @reps.clear
60
37
  end
61
38
 
62
- # Get the [5] index.
63
- def sixth
64
- self[5]
39
+ # Add a Representative to the reps array. Only accepts a Representative as an argument.
40
+ def add_rep(other)
41
+ if other.is_a?(Representative)
42
+ @reps << other
43
+ other.delegation = self unless other.delegation == self
44
+ else
45
+ not_a_rep_error
46
+ end
65
47
  end
66
48
 
67
- # Get the [6] index.
68
- def seventh
69
- self[6]
49
+ # Collects the OfficeLocations of all associated reps.
50
+ def office_locations
51
+ map(&:office_locations).flatten
70
52
  end
71
53
 
72
- # Get the [7] index.
73
- def eigth
74
- self[7]
54
+ # Collects the full names of all associated reps.
55
+ def names
56
+ map(&:name)
75
57
  end
76
58
 
77
- # Get the [8] index.
78
- def ninth
79
- self[8]
59
+ # Collects the first names of all associated reps.
60
+ def first_names
61
+ map(&:first_name)
80
62
  end
81
63
 
82
- # Get the [9] index.
83
- def tenth
84
- self[9]
64
+ # Collects the last names of all associated reps.
65
+ def last_names
66
+ map(&:last_name)
85
67
  end
86
68
  end
87
69
  end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+ module GetYourRep
3
+ # Custom Error handling classes and methods.
4
+ module Errors
5
+ # Errors for invalid or inadequate addresses.
6
+ class AddressError < ArgumentError
7
+ # Raise when the address is not a string or an integer.
8
+ # Supports integers for zip codes.
9
+ def invalid_address_type
10
+ 'Entry must be of types String or Integer'
11
+ end
12
+
13
+ # Raise when a rep cannot be found with the given parameters.
14
+ def reps_not_found
15
+ "Error message received. Some reps were not found. Confirm your address and check your parameters. \
16
+ Try a full address if you used a zip."
17
+ end
18
+ end
19
+
20
+ # Errors raised when type-specific operations are performed invalidly.
21
+ # These operations will generally be association-building operations.
22
+ class GetYourRepTypeError < TypeError
23
+ def not_a_rep # :nodoc:
24
+ 'Object must be a Representative.'
25
+ end
26
+
27
+ def not_an_office # :nodoc:
28
+ 'Object must be an OfficeLocation.'
29
+ end
30
+
31
+ def not_a_delegation # :nodoc:
32
+ 'Object must be a Delegation.'
33
+ end
34
+ end
35
+
36
+ # Error raised when CLI command is invalid.
37
+ class CommandNotFoundError < NoMethodError
38
+ def command_not_found # :nodoc:
39
+ "Command not found. Use 'help' command for a list of possible commands."
40
+ end
41
+ end
42
+
43
+ def not_an_office_error # :nodoc:
44
+ raise GetYourRepTypeError
45
+ rescue GetYourRepTypeError => error
46
+ puts error.not_an_office
47
+ end
48
+
49
+ def not_a_del_error # :nodoc:
50
+ raise GetYourRepTypeError
51
+ rescue GetYourRepTypeError => error
52
+ puts error.not_a_delegation
53
+ end
54
+
55
+ def not_a_rep_error # :nodoc:
56
+ raise GetYourRepTypeError
57
+ rescue GetYourRepTypeError => error
58
+ puts error.not_a_rep
59
+ end
60
+
61
+ def address_type_error # :nodoc:
62
+ raise AddressError
63
+ rescue AddressError => error
64
+ puts error.invalid_address_type.bold.red
65
+ end
66
+
67
+ def command_not_found_error # :nodoc:
68
+ raise CommandNotFoundError
69
+ rescue CommandNotFoundError => error
70
+ puts error.command_not_found.bold.red
71
+ end
72
+
73
+ def reps_not_found_error # :nodoc:
74
+ raise AddressError
75
+ rescue AddressError => error
76
+ puts error.reps_not_found.bold.red
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+ # GetYourRep module functions.
3
+ module GetYourRep
4
+ module_function
5
+
6
+ include GetYourRep::Errors
7
+
8
+ # Get all reps using Google for national legislators,
9
+ # and OpenStates for state reps.
10
+ def all_reps(address)
11
+ @delegation = Google.all_reps(address, congress_only: true)
12
+ open_states = OpenStates.all_reps(address)
13
+ @delegation.merge(open_states)
14
+ end
15
+
16
+ # Check if an address value is a zip code or a full address.
17
+ def address_is_a_zip?(address)
18
+ address = convert_int_address_to_string(address)
19
+ if address.is_a?(String)
20
+ address =~ /^\d{5}(-\d{4})?$/ ? true : false
21
+ else
22
+ address_type_error
23
+ end
24
+ end
25
+
26
+ # Convert a zip code to a street address using Geocoder to convert zip to coordinates
27
+ # and coordinates to an address.
28
+ def convert_zip_to_address(address)
29
+ @coordinates = Geocoder.coordinates(address)
30
+ address = Geocoder.address(@coordinates)
31
+ trim_street_number(address)
32
+ end
33
+
34
+ # If a street number is a range reduce it to one number.
35
+ def trim_street_number(address)
36
+ address = address.split(' ')
37
+ address[0] = address[0].split('-').shift
38
+ address.join(' ')
39
+ end
40
+
41
+ # Parses a String address and prepares for an HTTP request. Accepts Strings and Integers as args.
42
+ def parse_address(address)
43
+ address = convert_int_address_to_string(address)
44
+ if address.is_a?(String)
45
+ address.tr(',', '').split.join('%20')
46
+ else
47
+ address_type_error
48
+ end
49
+ end
50
+
51
+ # Geocode an address and returns the coordinates.
52
+ def get_coordinates(address)
53
+ address = convert_int_address_to_string(address)
54
+ if address.is_a?(String)
55
+ Geocoder.coordinates(address)
56
+ else
57
+ address_type_error
58
+ end
59
+ end
60
+
61
+ # Converts an integer to a string in case a zip was input as an int.
62
+ def convert_int_address_to_string(address)
63
+ if address.is_a?(Integer)
64
+ format('0%o', address)
65
+ else
66
+ address
67
+ end
68
+ end
69
+
70
+ # Creates an empty delegation if no reps were found.
71
+ def handle_reps_not_found_error
72
+ reps_not_found_error
73
+ puts response
74
+ Delegation.new
75
+ end
76
+ end
@@ -1,194 +1,72 @@
1
+ # frozen_string_literal: true
1
2
  module GetYourRep
2
-
3
3
  # Retrieve your elected representatives from the Google Civic Information API, parse it from
4
4
  # JSON, and assemble it into a usable Ruby Hash-like object called a Representative.
5
5
  # Representatives are then wrapped in an Array-like object called a Delegation.
6
6
  #
7
7
  # You must configure your own Google API key as an environment variable using the constant name
8
8
  # 'GOOGLE_API_KEY' in order for this gem to work.
9
- class Google
10
-
11
- # The Google API key is used to access data from the Google Civic Information API. You must
12
- # obtain your own and configure it as an environment variable on your system.
13
- API_KEY = ENV['GOOGLE_API_KEY']
14
-
15
- # Initiates a chain of class method calls that will instantiate a
16
- # Delegation object and return it.
17
- # Supported options for :level are "national"(default) and "state".
18
- # Supported options for :role are "representative"(default), "senator", "executive" and "vice executive".
19
- def self.now(address, level: 'national', role: 'representative')
20
- address = convert_zip_to_address(address) if address_is_a_zip?(address)
21
- @address = parse_address(address)
22
- @level = level
23
- @role = role
24
- get_rep
25
- end
26
-
27
- #Returns reps in Congress and State Legislature, plus Governor and Lieutenant Governor.
28
- def self.all(address)
29
- top_level_reps(address)
30
-
31
- @role = 'representative'
32
- state_rep = get_rep
33
-
34
- @role = 'senator'
35
- state_sen = get_rep
36
-
37
- #Check the Open States API if Google can't find state reps.
38
- if state_rep.empty? && state_sen.empty?
39
- @reps << GetYourRep::OpenStates.now(@coordinates)
40
- else
41
- @reps << state_rep
42
- @reps << state_sen
9
+ module Google
10
+ class << self
11
+ include GetYourRep
12
+
13
+ # Holds the parsed address for the HTTP request.
14
+ attr_accessor :address
15
+ # Holds the Delegation object which will be returned by the all_reps class method.
16
+ attr_accessor :delegation
17
+ # Holds the raw JSON data response from the API.
18
+ attr_accessor :response
19
+
20
+ # The Google API key is used to access data from the Google Civic Information API. You must
21
+ # obtain your own and configure it as an environment variable on your system.
22
+ API_KEY = ENV['GOOGLE_API_KEY']
23
+
24
+ # Makes the call to the Google API and delivers the response after parsing.
25
+ # Returns a GetYourRep::Delegation object, holding a collection of GetYourRep::Representatives
26
+ def all_reps(address, congress_only: nil)
27
+ init(address)
28
+ ask_google_api(congress_only: congress_only)
29
+ deliver_response
43
30
  end
44
31
 
45
- end
46
-
47
- def self.top_level_reps(address)
48
- @reps = now(address)
49
-
50
- @role = 'senator'
51
- @reps << get_rep
52
-
53
- @level = 'state'
54
- @role = 'executive'
55
- @reps << get_rep
56
-
57
- @role = 'vice executive'
58
- @reps << get_rep
59
- end
60
-
61
- # Parses a String address and prepares for an HTTP request. Accepts Strings and Integers as args.
62
- def self.parse_address(address)
63
- address = '0%o' % address if address.is_a?(Integer)
64
- raise "Entry must be of types String or Integer" if !address.is_a?(String)
65
- address = address.tr(',', '').split.join('%20')
66
- end
67
-
68
- # Unparses a parsed address
69
- # def self.unparsed_address(address)
70
- # address.split('%20').join(' ')
71
- # end
72
-
73
- # Check if an address value is a zip code or a full address.
74
- def self.address_is_a_zip?(address)
75
- address = '0%o' % address if address.is_a?(Integer)
76
- raise "Entry must be of types String or Integer" if !address.is_a?(String)
77
- !(address =~ /^\d{5}(-\d{4})?$/).nil?
78
- end
79
-
80
- # Convert a zip code to a street address using Geocoder to convert zip to coordinates
81
- # and coordinates to an address.
82
- def self.convert_zip_to_address(address)
83
- @coordinates = Geocoder.coordinates(address)
84
- address = Geocoder.address(@coordinates)
85
- trim_street_number(address)
86
- end
87
-
88
- # If a street number is a range reduce it to one number.
89
- def self.trim_street_number(address)
90
- address = address.split(' ')
91
- address[0] = address[0].split('-').shift
92
- address.join(' ')
93
- end
94
-
95
- # Sets parameters for and executes Google API request.
96
- def self.get_rep
32
+ private
97
33
 
98
- level = case @level
99
- when 'national'
100
- 'country'
101
- when 'state'
102
- 'administrativeArea1'
103
- else
104
- @level
105
- end
106
-
107
- role = case @role
108
- when 'representative'
109
- 'legislatorLowerBody'
110
- when 'senator'
111
- 'legislatorUpperBody'
112
- when 'executive'
113
- 'headOfGovernment'
114
- when 'vice executive'
115
- 'deputyHeadOfGovernment'
116
- else
117
- @role
118
- end
119
-
120
- url = "https://www.googleapis.com/civicinfo/v2/representatives?address=#{@address}%20&includeOffices=true&levels=#{level}&roles=#{role}&fields=offices%2Cofficials&key=#{API_KEY}"
121
- @response = HTTParty.get(url).parsed_response
122
- deliver_response
123
- end
124
-
125
- def self.deliver_response
126
- if @response.empty?
127
- puts "'Google can't find a rep with that address. Checking the Open States API."
128
- return GetYourRep::Delegation.new
129
- elsif @response['error']
130
- puts 'Error message received. Confirm and re-enter your address and check your parameters.'
131
- puts @response
132
- return GetYourRep::Delegation.new
34
+ def init(address)
35
+ address = convert_zip_to_address(address) if address_is_a_zip?(address)
36
+ self.address = parse_address(address)
133
37
  end
134
- parse_rep
135
- @delegation
136
- end
137
38
 
138
- # Parses the JSON response and assembles it into a Delegation object,
139
- # except for social media attributes, which are handles by .parse_channels.
140
- def self.parse_rep
141
-
142
- @delegation = GetYourRep::Delegation.new
143
- @officials = @response['officials']
144
-
145
- @officials.each do |official|
146
- @delegation << GetYourRep::Representative[
147
- :name, official['name'],
148
- :office, @response['offices'].first['name'],
149
- :party, official['party'],
150
- :phone, official['phones'] || [],
151
- :office_locations, offices(official),
152
- :email, official['emails'] || [],
153
- :url, (official['urls'].first if official['urls']),
154
- :photo, official['photoUrl'],
155
- ]
156
- parse_channels(official) if official['channels']
39
+ def ask_google_api(congress_only: nil)
40
+ if congress_only
41
+ url = "https://www.googleapis.com/civicinfo/v2/representatives?address=#{address}%20&includeOffices=true&\
42
+ levels=country&roles=legislatorLowerBody&roles=legislatorUpperBody&fields=offices%2Cofficials&key=#{API_KEY}"
43
+ else
44
+ url = "https://www.googleapis.com/civicinfo/v2/representatives?address=#{address}%20&includeOffices=true&\
45
+ levels=country&levels=administrativeArea1&roles=legislatorLowerBody&roles=legislatorUpperBody&roles=headOfGovernment\
46
+ &roles=deputyHeadOfGovernment&fields=offices%2Cofficials&key=#{API_KEY}"
47
+ end
48
+ self.response = HTTParty.get(url).parsed_response
157
49
  end
158
- end
159
-
160
- def self.offices(official)
161
- offices = []
162
50
 
163
- if official['address']
164
- official['address'].each do |office|
165
- office_hash = {}
166
-
167
- if office['line1'].downcase.match(/(state|house|senate|assembly|capitol|dirksen|reyburn|rayburn|legislative|legislature|government)+/)
168
- office_hash[:type] = 'capitol'
169
- else
170
- office_hash[:type] = 'district'
171
- end
172
-
173
- office_hash[:line_1] = office['line1']
174
- office_hash[:line_2] = office['line_2']
175
- office_hash[:line_3] = "#{office['city'].capitalize}, #{office['state']} #{office['zip']}"
176
- offices << office_hash
51
+ def deliver_response
52
+ if response.empty? || response['error']
53
+ handle_reps_not_found_error
54
+ else
55
+ parse_reps
56
+ delegation
177
57
  end
178
58
  end
179
59
 
180
- offices
181
- end
60
+ def parse_reps
61
+ self.delegation = Delegation.new
182
62
 
183
- # Parses social media handles and adds them to the Delegation object.
184
- def self.parse_channels(official)
185
- official['channels'].each do |channel|
186
- @delegation.last[channel['type'].downcase.to_sym] = channel['id']
63
+ response['officials'].each_with_index do |official, index|
64
+ external_rep = GoogleRep.new(official)
65
+ rep_hash = external_rep.build_hash(response['offices'], index)
66
+ new_rep = Representative.new(rep_hash)
67
+ delegation.add_rep(new_rep)
68
+ end
187
69
  end
188
- @delegation.last[:twitter] = nil unless @delegation.last[:twitter]
189
- @delegation.last[:facebook] = nil unless @delegation.last[:facebook]
190
- @delegation.last[:youtube] = nil unless @delegation.last[:youtube]
191
- @delegation.last[:googleplus] = nil unless @delegation.last[:googleplus]
192
70
  end
193
71
  end
194
72
  end