get_your_rep 0.1.9 → 1.0.0

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