parliament-ruby 0.5.19 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -3
  3. data/.travis.yml +0 -1
  4. data/Gemfile +2 -0
  5. data/Makefile +9 -4
  6. data/README.md +185 -11
  7. data/Rakefile +1 -1
  8. data/lib/parliament/client_error.rb +21 -0
  9. data/lib/parliament/decorator/constituency_area.rb +27 -0
  10. data/lib/parliament/{decorators → decorator}/constituency_group.rb +29 -1
  11. data/lib/parliament/decorator/contact_point.rb +48 -0
  12. data/lib/parliament/decorator/gender.rb +13 -0
  13. data/lib/parliament/decorator/gender_identity.rb +13 -0
  14. data/lib/parliament/decorator/house.rb +41 -0
  15. data/lib/parliament/decorator/house_incumbency.rb +50 -0
  16. data/lib/parliament/decorator/house_seat.rb +27 -0
  17. data/lib/parliament/decorator/incumbency.rb +57 -0
  18. data/lib/parliament/decorator/party.rb +27 -0
  19. data/lib/parliament/decorator/party_membership.rb +36 -0
  20. data/lib/parliament/decorator/person.rb +224 -0
  21. data/lib/parliament/{decorators → decorator}/postal_address.rb +5 -1
  22. data/lib/parliament/decorator/seat_incumbency.rb +64 -0
  23. data/lib/parliament/decorator.rb +7 -0
  24. data/lib/parliament/network_error.rb +13 -0
  25. data/lib/parliament/no_content_response_error.rb +19 -0
  26. data/lib/parliament/request.rb +112 -33
  27. data/lib/parliament/response.rb +76 -9
  28. data/lib/parliament/server_error.rb +21 -0
  29. data/lib/parliament/utils.rb +113 -13
  30. data/lib/parliament/version.rb +1 -1
  31. data/lib/parliament.rb +8 -4
  32. data/parliament-ruby.gemspec +6 -6
  33. metadata +32 -28
  34. data/lib/parliament/decorators/constituency_area.rb +0 -17
  35. data/lib/parliament/decorators/contact_point.rb +0 -29
  36. data/lib/parliament/decorators/gender.rb +0 -9
  37. data/lib/parliament/decorators/gender_identity.rb +0 -9
  38. data/lib/parliament/decorators/house.rb +0 -28
  39. data/lib/parliament/decorators/house_incumbency.rb +0 -31
  40. data/lib/parliament/decorators/house_seat.rb +0 -17
  41. data/lib/parliament/decorators/incumbency.rb +0 -35
  42. data/lib/parliament/decorators/party.rb +0 -17
  43. data/lib/parliament/decorators/party_membership.rb +0 -23
  44. data/lib/parliament/decorators/person.rb +0 -152
  45. data/lib/parliament/decorators/seat_incumbency.rb +0 -39
  46. data/lib/parliament/no_content_error.rb +0 -9
@@ -1,6 +1,10 @@
1
1
  module Parliament
2
- module Decorators
2
+ module Decorator
3
+ # Decorator namespace for Grom::Node instances with type: http://id.ukpds.org/schema/PostalAddress
3
4
  module PostalAddress
5
+ # Builds a full address using the lines of the address and the postcode.
6
+ #
7
+ # @return [String, String] the full address of the Grom::Node or an empty string.
4
8
  def full_address
5
9
  address_array.join(', ')
6
10
  end
@@ -0,0 +1,64 @@
1
+ module Parliament
2
+ module Decorator
3
+ # Decorator namespace for Grom::Node instances with type: http://id.ukpds.org/schema/SeatIncumbency
4
+ module SeatIncumbency
5
+ # Alias incumbencyStartDate with fallback.
6
+ #
7
+ # @return [DateTime, nil] the start date of the Grom::Node or nil.
8
+ def start_date
9
+ respond_to?(:incumbencyStartDate) ? DateTime.parse(incumbencyStartDate) : nil
10
+ end
11
+
12
+ # Alias incumbencyEndDate with fallback.
13
+ #
14
+ # @return [DateTime, nil] the end date of the Grom::Node or nil.
15
+ def end_date
16
+ respond_to?(:incumbencyEndDate) ? DateTime.parse(incumbencyEndDate) : nil
17
+ end
18
+
19
+ # Alias seatIncumbencyHasHouseSeat with fallback.
20
+ #
21
+ # @return [Grom::Node, nil] the seat of the Grom::Node or nil.
22
+ def seat
23
+ respond_to?(:seatIncumbencyHasHouseSeat) ? seatIncumbencyHasHouseSeat.first : nil
24
+ end
25
+
26
+ # Checks if Grom::Node has an end date.
27
+ #
28
+ # @return [Boolean] a boolean depending on whether or not the Grom::Node has an end date.
29
+ def current?
30
+ has_end_date = respond_to?(:incumbencyEndDate)
31
+
32
+ !has_end_date
33
+ end
34
+
35
+ # Alias houseSeatHasHouse with fallback.
36
+ #
37
+ # @return [Grom::Node, nil] the house of the Grom::Node or nil.
38
+ def house
39
+ seat.nil? ? nil : seat.house
40
+ end
41
+
42
+ # Alias houseSeatHasConstituencyGroup with fallback.
43
+ #
44
+ # @return [Grom::Node, nil] the constituency of the Grom::Node or nil.
45
+ def constituency
46
+ seat.nil? ? nil : seat.constituency
47
+ end
48
+
49
+ # Alias incumbencyHasContactPoint with fallback.
50
+ #
51
+ # @return [Array, Array] the contact points of the Grom::Node or an empty array.
52
+ def contact_points
53
+ respond_to?(:incumbencyHasContactPoint) ? incumbencyHasContactPoint : []
54
+ end
55
+
56
+ # Alias incumbencyHasMember with fallback.
57
+ #
58
+ # @return [Grom::Node, nil] the member connected to the Grom::Node or nil.
59
+ def member
60
+ respond_to?(:incumbencyHasMember) ? incumbencyHasMember.first : nil
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,7 @@
1
+ module Parliament
2
+ # Decorator namespace
3
+ module Decorator
4
+ # require all the decorators
5
+ Dir[File.join(File.dirname(__FILE__), '../parliament/decorator', '*.rb')].each { |decorator| require decorator }
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ module Parliament
2
+ # A parent class that standardises the error message generated for network errors.
3
+ #
4
+ # @see Parliament::ClientError
5
+ # @see Parliament::ServerError
6
+ #
7
+ # @since 0.6.0
8
+ class NetworkError < StandardError
9
+ def initialize(url, response)
10
+ super("#{response.code} HTTP status code received from: #{url} - #{response.message}")
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ module Parliament
2
+ # An error raised when a 204 status code is returned by Net::HTTP inside of Parliament::Request.
3
+ #
4
+ # @since 0.6.0
5
+ class NoContentResponseError < Parliament::NetworkError
6
+ # @param [String] url the url that caused the Parliament::ClientError
7
+ # @param [Net::HTTPResponse] response the Net:HTTPResponse that caused the Parliament::NoContentResponseError
8
+ #
9
+ # @example Creating a Parliament::NoContentResponseError
10
+ # url = 'http://localhost:3030/foo/bar'
11
+ #
12
+ # response = Net::HTTP.get_response(URI(url))
13
+ #
14
+ # raise Parliament::NoContentResponseError.new(url, response) if response.code == '204'
15
+ def initialize(url, response)
16
+ super
17
+ end
18
+ end
19
+ end
@@ -1,26 +1,109 @@
1
1
  module Parliament
2
+ # API request object, allowing the user to build a request to a graph-based API, download n-triple data and create
3
+ # ruby objects from that data.
4
+ #
5
+ # @since 0.1.0
6
+ #
7
+ # @attr_reader [String] base_url the base url of our api. (expected: http://example.com - without the trailing slash).
2
8
  class Request
3
9
  attr_reader :base_url
4
10
 
11
+ # Creates a new instance of Parliament::Request.
12
+ #
13
+ # An interesting note for #initialize is that setting base_url on the class, or using the environment variable
14
+ # PARLIAMENT_BASE_URL means you don't need to pass in a base_url. You can pass one anyway to override the
15
+ # environment variable or class parameter.
16
+ #
17
+ # @example Setting the base_url on the class
18
+ # Parliament::Request.base_url = 'http://example.com'
19
+ #
20
+ # Parliament::Request.new.base_url #=> 'http://example.com'
21
+ #
22
+ # @example Setting the base_url via environment variable
23
+ # ENV['PARLIAMENT_BASE_URL'] #=> 'http://test.com'
24
+ #
25
+ # Parliament::Request.new.base_url #=> 'http://test.com'
26
+ #
27
+ # @example Setting the base_url via a parameter
28
+ # Parliament::Request.base_url #=> nil
29
+ # ENV['PARLIAMENT_BASE_URL'] #=> nil
30
+ #
31
+ # Parliament::Request.new(base_url: 'http://example.com').base_url #=> 'http://example.com'
32
+ #
33
+ # @example Overriding the base_url via a parameter
34
+ # ENV['PARLIAMENT_BASE_URL'] #=> 'http://test.com'
35
+ #
36
+ # Parliament::Request.new(base_url: 'http://example.com').base_url #=> 'http://example.com'
37
+ #
38
+ # @param [String] base_url the base url of our api. (expected: http://example.com - without the trailing slash).
5
39
  def initialize(base_url: nil)
6
40
  @endpoint_parts = []
7
41
  @base_url = base_url || self.class.base_url || ENV['PARLIAMENT_BASE_URL']
8
42
  end
9
43
 
44
+ # Overrides ruby's method_missing to allow creation of URLs through method calls.
45
+ #
46
+ # @example Adding a simple URL part
47
+ # request = Parliament::Request.new(base_url: 'http://example.com')
48
+ #
49
+ # # url: http://example.com/people
50
+ # request.people
51
+ #
52
+ # @example Adding a simple URL part with parameters
53
+ # request = Parliament::Request.new(base_url: 'http://example.com')
54
+ #
55
+ # # url: http://example.com/people/123456
56
+ # request.people('123456')
57
+ #
58
+ # @example Chaining URL parts and using hyphens
59
+ # request = Parliament::Request.new(base_url: 'http://example.com')
60
+ #
61
+ # # url: http://example.com/people/123456/foo/bar/hello-world/7890
62
+ # request.people('123456').foo.bar('hello-world', '7890')
63
+ #
64
+ # @param [Symbol] method the 'method' (url part) we are processing.
65
+ # @param [Array<Object>] params parameters passed to the specified method (url part).
66
+ # @param [Block] block additional block (kept for compatibility with method_missing API).
67
+ #
68
+ # @return [Parliament::Request] self.
10
69
  def method_missing(method, *params, &block)
11
- # TODO: Fix this smell
12
- super if method == :base_url=
13
-
14
70
  @endpoint_parts << method.to_s
15
71
  @endpoint_parts << params
16
72
  @endpoint_parts = @endpoint_parts.flatten!
17
- self
73
+
74
+ block&.call
75
+
76
+ self || super
18
77
  end
19
78
 
20
- def respond_to_missing?(method, include_private = false)
21
- (method != :base_url=) || super
79
+ # This class always responds to method calls, even those missing. Therefore, respond_to_missing? always returns true.
80
+ #
81
+ # @return [Boolean] always returns true.
82
+ def respond_to_missing?(_, _ = false)
83
+ true # responds to everything, always
22
84
  end
23
85
 
86
+ # Using our url built via #method_missing, make a HTTP GET request and process results into a response.
87
+ #
88
+ # @example HTTP GET request
89
+ # request = Parliament::Request.new(base_url: 'http://example.com')
90
+ #
91
+ # # url: http://example.com/people/123456
92
+ # response = request.people('123456').get #=> #<Parliament::Response ...>
93
+ #
94
+ # @example HTTP GET request with URI encoded form values
95
+ # request = Parliament::Request.new(base_url: 'http://example.com')
96
+ #
97
+ # # url: http://example.com/people/current?limit=10&page=4&lang=en-gb
98
+ # response = request.people.current.get({ limit: 10, page: 4, lang: 'en-gb' }) #=> #<Parliament::Response ...>
99
+ #
100
+ # @raise [Parliament::ServerError] when the server responds with a 5xx status code.
101
+ # @raise [Parliament::ClientError] when the server responds with a 4xx status code.
102
+ # @raise [Parliament::NoContentResponseError] when the server responds with a 204 status code.
103
+ #
104
+ # @param [Hash] params (optional) additional URI encoded form values to be added to the URI.
105
+ #
106
+ # @return [Parliament::Response] a Parliament::Response object containing all of the nodes returned from the URL.
24
107
  def get(params: nil)
25
108
  endpoint_uri = URI.parse(api_endpoint)
26
109
  endpoint_uri.query = URI.encode_www_form(params.to_a) unless params.nil?
@@ -32,29 +115,35 @@ module Parliament
32
115
  build_parliament_response(net_response)
33
116
  end
34
117
 
35
- def build_parliament_response(response)
36
- objects = Grom::Reader.new(response.body).objects
37
- objects.map { |object| assign_decorator(object) }
118
+ private
38
119
 
39
- Parliament::Response.new(objects)
120
+ # @attr [String] base_url the base url of our api. (expected: http://example.com - without the trailing slash).
121
+ class << self
122
+ attr_accessor :base_url
40
123
  end
41
124
 
42
- def handle_errors(response)
43
- handle_not_found_error(response)
44
- handle_server_error(response)
45
- handle_no_content_error(response)
125
+ def api_endpoint
126
+ [@base_url, @endpoint_parts].join('/')
46
127
  end
47
128
 
48
- def handle_server_error(response)
49
- raise StandardError, 'This is a HTTPServerError' if response.is_a?(Net::HTTPServerError)
129
+ def handle_errors(response)
130
+ case response
131
+ when Net::HTTPSuccess # 2xx Status
132
+ exception_class = Parliament::NoContentResponseError if response.code == '204'
133
+ when Net::HTTPClientError # 4xx Status
134
+ exception_class = Parliament::ClientError
135
+ when Net::HTTPServerError # 5xx Status
136
+ exception_class = Parliament::ServerError
137
+ end
138
+
139
+ raise exception_class.new(api_endpoint, response) if exception_class
50
140
  end
51
141
 
52
- def handle_not_found_error(response)
53
- raise StandardError, 'This is a HTTPClientError' if response.is_a?(Net::HTTPClientError)
54
- end
142
+ def build_parliament_response(response)
143
+ objects = Grom::Reader.new(response.body).objects
144
+ objects.map { |object| assign_decorator(object) }
55
145
 
56
- def handle_no_content_error(response)
57
- raise Parliament::NoContentError if response.code == '204'
146
+ Parliament::Response.new(objects)
58
147
  end
59
148
 
60
149
  def assign_decorator(object)
@@ -62,20 +151,10 @@ module Parliament
62
151
 
63
152
  object_type = Grom::Helper.get_id(object.type)
64
153
 
65
- return object unless Parliament::Decorators.constants.include?(object_type.to_sym)
154
+ return object unless Parliament::Decorator.constants.include?(object_type.to_sym)
66
155
 
67
- decorator_module = Object.const_get("Parliament::Decorators::#{object_type}")
156
+ decorator_module = Object.const_get("Parliament::Decorator::#{object_type}")
68
157
  object.extend(decorator_module)
69
158
  end
70
-
71
- private
72
-
73
- class << self
74
- attr_accessor :base_url
75
- end
76
-
77
- def api_endpoint
78
- [@base_url, @endpoint_parts].join('/')
79
- end
80
159
  end
81
160
  end
@@ -1,16 +1,65 @@
1
1
  require 'forwardable'
2
2
 
3
3
  module Parliament
4
+ # API response object that wraps an Array of Grom::Node objects with common helper methods.
5
+ #
6
+ # Delegates a number of common methods to the array of Grom::Nodes including, but not limited to, :size, :each, :map, :count etc.
7
+ #
8
+ # @since 0.1.0
9
+ #
10
+ # @attr_reader [Array<Grom::Node>] nodes Graph nodes.
4
11
  class Response
5
12
  include Enumerable
6
13
  extend Forwardable
7
14
  attr_reader :nodes
8
15
  def_delegators :@nodes, :size, :each, :select, :map, :select!, :map!, :count, :length, :[], :empty?
9
16
 
17
+ # @param [Array<Grom::Node>] nodes An array of nodes the response should wrap
10
18
  def initialize(nodes)
11
19
  @nodes = nodes
12
20
  end
13
21
 
22
+ # Given our array of Grom::Nodes, filter them into arrays of 'types' of nodes.
23
+ #
24
+ # Note: this method assumes all of your nodes include a #type attribute.
25
+ #
26
+ # @since 0.2.0
27
+ #
28
+ # @example Filtering for a single type
29
+ # node_1 = Grom::Node.new
30
+ # node_1.instance_variable_set(:type, 'type_1')
31
+ # node_2 = Grom::Node.new
32
+ # node_2.instance_variable_set(:type, 'type_3')
33
+ # node_3 = Grom::Node.new
34
+ # node_3.instance_variable_set(:type, 'type_1')
35
+ # node_4 = Grom::Node.new
36
+ # node_4.instance_variable_set(:type, 'type_2')
37
+ # nodes = [node_1, node_2, node_3, node_4]
38
+ #
39
+ # response = Parliament::Response.new(nodes)
40
+ # response.filter('type_2') #=> [#<Grom::Node @type='type_2'>]
41
+ #
42
+ # @example Filtering for multiple types
43
+ # node_1 = Grom::Node.new
44
+ # node_1.instance_variable_set(:type, 'type_1')
45
+ # node_2 = Grom::Node.new
46
+ # node_2.instance_variable_set(:type, 'type_3')
47
+ # node_3 = Grom::Node.new
48
+ # node_3.instance_variable_set(:type, 'type_1')
49
+ # node_4 = Grom::Node.new
50
+ # node_4.instance_variable_set(:type, 'type_2')
51
+ # nodes = [node_1, node_2, node_3, node_4]
52
+ #
53
+ # response = Parliament::Response.new(nodes)
54
+ # response.filter('type_2', 'type_1') #=> [[#<Grom::Node @type='type_2'>], [#<Grom::Node @type='type_1'>, #<Grom::Node @type='type_1'>]]
55
+ #
56
+ # # Also consider
57
+ # type_2, type_1 = response.filter('type_2', 'type_1')
58
+ # type_2 #=> [#<Grom::Node @type='type_2'>]
59
+ # type_1 #=> [#<Grom::Node @type='type_1'>, #<Grom::Node @type='type_1'>]
60
+ #
61
+ # @param [Array<String>] types An array of type strings that you are looking for.
62
+ # @return [Array<Grom::Node> || Array<*Array<Grom::Node>>] If you pass one type, this returns an Array of Grom::Node objects. If you pass multiple, it returns an array, of arrays of Grom::Node objects.
14
63
  def filter(*types)
15
64
  filtered_objects = Array.new(types.size) { [] }
16
65
 
@@ -26,15 +75,14 @@ module Parliament
26
75
  types.size == 1 ? result.first : result
27
76
  end
28
77
 
29
- def build_responses(filtered_objects)
30
- result = []
31
-
32
- filtered_objects.each do |objects|
33
- result << Parliament::Response.new(objects)
34
- end
35
- result
36
- end
37
-
78
+ # Sort the Parliament::Response nodes in ascending order by a set of attributes on each node.
79
+ #
80
+ # @see Parliament::Utils.sort_by
81
+ #
82
+ # @since 0.5.0
83
+ #
84
+ # @param [Array<Symbol>] parameters Attributes to sort on - left to right.
85
+ # @return [Array<Grom::Node>] A sorted array of nodes.
38
86
  def sort_by(*parameters)
39
87
  Parliament::Utils.sort_by({
40
88
  list: @nodes,
@@ -42,11 +90,30 @@ module Parliament
42
90
  })
43
91
  end
44
92
 
93
+ # Sort the Parliament::Response nodes in descending order by a set of attributes on each node.
94
+ #
95
+ # @see Parliament::Utils.reverse_sort_by
96
+ #
97
+ # @since 0.5.0
98
+ #
99
+ # @param [Array<Symbol>] parameters Attributes to sort on - left to right.
100
+ # @return [Array<Grom::Node>] A sorted array of nodes.
45
101
  def reverse_sort_by(*parameters)
46
102
  Parliament::Utils.reverse_sort_by({
47
103
  list: @nodes,
48
104
  parameters: parameters
49
105
  })
50
106
  end
107
+
108
+ private
109
+
110
+ def build_responses(filtered_objects)
111
+ result = []
112
+
113
+ filtered_objects.each do |objects|
114
+ result << Parliament::Response.new(objects)
115
+ end
116
+ result
117
+ end
51
118
  end
52
119
  end
@@ -0,0 +1,21 @@
1
+ module Parliament
2
+ # An error raised when a 5xx status code is returned by Net::HTTP inside of Parliament::Request.
3
+ #
4
+ # @see Parliament::ClientError
5
+ #
6
+ # @since 0.6.0
7
+ class ServerError < Parliament::NetworkError
8
+ # @param [String] url the url that caused the Parliament::ServerError
9
+ # @param [Net::HTTPServerError] response the Net:HTTPServerError that caused the Parliament::ServerError
10
+ #
11
+ # @example Creating a Parliament::ServerError
12
+ # url = 'http://localhost:3030/foo/bar'
13
+ #
14
+ # response = Net::HTTP.get_response(URI(url))
15
+ #
16
+ # raise Parliament::ServerError.new(url, response) if response.is_a?(Net::HTTPServerError)
17
+ def initialize(url, response)
18
+ super
19
+ end
20
+ end
21
+ end