1+ require 'faraday'
2+ require 'nokogiri'
13require 'postcode_sanitizer'
24
35module ConstituencyApi
4- class Error < RuntimeError ; end
5-
66 class Constituency
77 attr_reader :id , :name , :mp
88
@@ -18,73 +18,121 @@ def ==(other)
1818
1919 class Mp
2020 attr_reader :id , :name , :start_date
21- URL = "http://www.parliament.uk/biographies/commons"
21+ URL = "http://www.parliament.uk/biographies/commons/%{name}/%{id} "
2222
2323 def initialize ( id , name , start_date )
2424 @id , @name , @start_date = id , name , start_date . to_date
2525 end
2626
2727 def url
28- " #{ URL } / #{ name . parameterize } / #{ id } "
28+ URL % { name : name . parameterize , id : id }
2929 end
3030
3131 def ==( other )
32- other . is_a? ( self . class ) && id == other . id && name == other . name && start_date && other . start_date
32+ other . is_a? ( self . class ) && id == other . id && name == other . name && start_date == other . start_date
3333 end
3434 alias_method :eql? , :==
3535 end
3636
3737 class Client
38- include Faraday
39- URL = 'http://data.parliament.uk/ membersdataplatform/services/mnis/Constituencies'
38+ API_HOST = 'http://data.parliament.uk'
39+ API_ENDPOINT = '/ membersdataplatform/services/mnis/Constituencies/%{postcode}/ '
4040 TIMEOUT = 5
4141
42- def self . constituency ( postcode )
43- constituencies ( postcode ) . first
42+ def call ( postcode )
43+ faraday . get ( path ( postcode ) ) do |request |
44+ request . options [ :timeout ] = TIMEOUT
45+ request . options [ :open_timeout ] = TIMEOUT
46+ end
4447 end
4548
46- def self . constituencies ( postcode )
47- response = call_api ( postcode )
48- parse_constituencies ( response )
49+ private
50+
51+ def faraday
52+ Faraday . new ( API_HOST ) do |f |
53+ f . response :follow_redirects
54+ f . response :raise_error
55+ f . adapter Faraday . default_adapter
56+ end
4957 end
5058
51- def self . parse_constituencies ( response )
52- return [ ] unless response [ "Constituencies" ]
53- constituencies = response [ "Constituencies" ] [ "Constituency" ]
54- Array . wrap ( constituencies ) . map { |c | Constituency . new ( c [ "Constituency_Id" ] , c [ "Name" ] , last_mp ( c ) ) }
59+ def path ( postcode )
60+ API_ENDPOINT % { postcode : escape_path ( postcode ) }
5561 end
5662
57- def self . call_api ( postcode )
58- sanitized_postcode = PostcodeSanitizer . call ( postcode )
59- response = Faraday . new ( URL ) . get ( "#{ sanitized_postcode } /" ) do |req |
60- req . options [ :timeout ] = TIMEOUT
61- req . options [ :open_timeout ] = TIMEOUT
62- end
63- unless response . status == 200
64- raise Error . new ( "Unexpected response from API:" \
65- "status #{ response . status } " \
66- "body #{ response . body } " \
67- "request #{ URL } /#{ sanitized_postcode } /" )
63+ def escape_path ( value )
64+ Rack ::Utils . escape_path ( value )
65+ end
66+ end
67+
68+ class Query
69+ CONSTITUENCIES = '//Constituencies/Constituency'
70+ CONSTITUENCY_ID = './Constituency_Id'
71+ CONSTITUENCY_NAME = './Name'
72+
73+ CURRENT_MP = './RepresentingMembers/RepresentingMember[1]'
74+ MP_ID = './Member_Id'
75+ MP_NAME = './Member'
76+ MP_DATE = './StartDate'
77+
78+ def initialize ( postcode )
79+ @postcode = postcode
80+ end
81+
82+ def fetch
83+ response = client . call ( postcode )
84+
85+ if response . success?
86+ parse ( response . body )
87+ else
88+ [ ]
6889 end
69- Hash . from_xml ( response . body )
70- rescue Faraday ::Error ::TimeoutError
71- raise Error . new ( "Timeout after #{ TIMEOUT } seconds" )
90+ rescue Faraday ::Error ::ResourceNotFound => e
91+ return [ ]
7292 rescue Faraday ::Error => e
73- raise Error . new ( "Network error - #{ e } " )
93+ Appsignal . send_exception ( e ) if defined? ( Appsignal )
94+ return [ ]
7495 end
7596
76- def self . last_mp ( constituency_hash )
77- mps = parse_mps ( constituency_hash )
78- mps . select ( &:start_date ) . sort_by ( &:start_date ) . last
97+ private
98+
99+ def client
100+ @client ||= Client . new
79101 end
80102
81- def self . parse_mps ( response )
82- return [ ] unless response [ "RepresentingMembers" ]
83- mps = response [ "RepresentingMembers" ] [ "RepresentingMember" ]
84- Array . wrap ( mps ) . map { |m | Mp . new ( m [ "Member_Id" ] , m [ "Member" ] , m [ "StartDate" ] ) }
103+ def postcode
104+ PostcodeSanitizer . call ( @postcode )
85105 end
86106
87- private_class_method :parse_constituencies , :call_api , :parse_mps , :last_mp
107+ def parse ( body )
108+ xml = Nokogiri ::XML ( body )
109+
110+ xml . xpath ( CONSTITUENCIES ) . map do |node |
111+ id = node . xpath ( CONSTITUENCY_ID ) . text
112+ name = node . xpath ( CONSTITUENCY_NAME ) . text
113+
114+ if mp = node . at_xpath ( CURRENT_MP )
115+ Constituency . new ( id , name , parse_mp ( mp ) )
116+ else
117+ Constituency . new ( id , name )
118+ end
119+ end
120+ end
121+
122+ def parse_mp ( node )
123+ id = node . xpath ( MP_ID ) . text
124+ name = node . xpath ( MP_NAME ) . text
125+ date = node . xpath ( MP_DATE ) . text
126+
127+ Mp . new ( id , name , date )
128+ end
129+ end
130+
131+ def self . constituency ( postcode )
132+ constituencies ( postcode ) . first
88133 end
89- end
90134
135+ def self . constituencies ( postcode )
136+ Query . new ( postcode ) . fetch
137+ end
138+ end
0 commit comments