whos_got_dirt 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +6 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +20 -0
  5. data/.yardopts +3 -0
  6. data/Gemfile +7 -0
  7. data/LICENSE +20 -0
  8. data/README.md +61 -0
  9. data/Rakefile +69 -0
  10. data/lib/whos_got_dirt/renderer.rb +66 -0
  11. data/lib/whos_got_dirt/request.rb +218 -0
  12. data/lib/whos_got_dirt/requests/entity/corp_watch.rb +123 -0
  13. data/lib/whos_got_dirt/requests/entity/little_sis.rb +52 -0
  14. data/lib/whos_got_dirt/requests/entity/open_corporates.rb +104 -0
  15. data/lib/whos_got_dirt/requests/entity/open_duka.rb +33 -0
  16. data/lib/whos_got_dirt/requests/entity/poderopedia.rb +48 -0
  17. data/lib/whos_got_dirt/requests/entity.rb +54 -0
  18. data/lib/whos_got_dirt/requests/list/little_sis.rb +37 -0
  19. data/lib/whos_got_dirt/requests/list/open_corporates.rb +36 -0
  20. data/lib/whos_got_dirt/requests/list.rb +16 -0
  21. data/lib/whos_got_dirt/requests/relation/open_corporates.rb +72 -0
  22. data/lib/whos_got_dirt/requests/relation/open_oil.rb +49 -0
  23. data/lib/whos_got_dirt/requests/relation.rb +51 -0
  24. data/lib/whos_got_dirt/response.rb +79 -0
  25. data/lib/whos_got_dirt/responses/entity/corp_watch.rb +72 -0
  26. data/lib/whos_got_dirt/responses/entity/little_sis.rb +66 -0
  27. data/lib/whos_got_dirt/responses/entity/open_corporates.rb +61 -0
  28. data/lib/whos_got_dirt/responses/entity/open_duka.rb +42 -0
  29. data/lib/whos_got_dirt/responses/entity/poderopedia.rb +44 -0
  30. data/lib/whos_got_dirt/responses/entity.rb +7 -0
  31. data/lib/whos_got_dirt/responses/helpers/little_sis.rb +34 -0
  32. data/lib/whos_got_dirt/responses/helpers/open_corporates.rb +28 -0
  33. data/lib/whos_got_dirt/responses/list/little_sis.rb +42 -0
  34. data/lib/whos_got_dirt/responses/list/open_corporates.rb +34 -0
  35. data/lib/whos_got_dirt/responses/list.rb +7 -0
  36. data/lib/whos_got_dirt/responses/relation/open_corporates.rb +66 -0
  37. data/lib/whos_got_dirt/responses/relation/open_oil.rb +70 -0
  38. data/lib/whos_got_dirt/responses/relation.rb +7 -0
  39. data/lib/whos_got_dirt/result.rb +106 -0
  40. data/lib/whos_got_dirt/validator.rb +52 -0
  41. data/lib/whos_got_dirt/version.rb +3 -0
  42. data/lib/whos_got_dirt.rb +43 -0
  43. data/schemas/popolo.json +1619 -0
  44. data/spec/cassettes/corp_watch_entity.yml +35 -0
  45. data/spec/cassettes/little_sis_entity.yml +98 -0
  46. data/spec/cassettes/little_sis_list.yml +159 -0
  47. data/spec/cassettes/open_corporates_entity.yml +137 -0
  48. data/spec/cassettes/open_corporates_list.yml +60 -0
  49. data/spec/cassettes/open_corporates_relation.yml +121 -0
  50. data/spec/cassettes/open_duka_entity.yml +1149 -0
  51. data/spec/cassettes/open_oil_relation.yml +640 -0
  52. data/spec/cassettes/poderopedia_entity.yml +43 -0
  53. data/spec/renderer_spec.rb +48 -0
  54. data/spec/request_spec.rb +205 -0
  55. data/spec/requests/entity/corp_watch_spec.rb +99 -0
  56. data/spec/requests/entity/little_sis_spec.rb +33 -0
  57. data/spec/requests/entity/open_corporates_spec.rb +81 -0
  58. data/spec/requests/entity/open_duka_spec.rb +21 -0
  59. data/spec/requests/entity/poderopedia_spec.rb +21 -0
  60. data/spec/requests/list/little_sis_spec.rb +25 -0
  61. data/spec/requests/list/open_corporates_spec.rb +33 -0
  62. data/spec/requests/relation/open_corporates_spec.rb +53 -0
  63. data/spec/requests/relation/open_oil_spec.rb +37 -0
  64. data/spec/response_spec.rb +119 -0
  65. data/spec/responses/entity/corp_watch_spec.rb +31 -0
  66. data/spec/responses/entity/little_sis_spec.rb +31 -0
  67. data/spec/responses/entity/open_corporates_spec.rb +31 -0
  68. data/spec/responses/entity/open_duka_spec.rb +25 -0
  69. data/spec/responses/entity/poderopedia_spec.rb +25 -0
  70. data/spec/responses/list/little_sis_spec.rb +31 -0
  71. data/spec/responses/list/open_corporates_spec.rb +31 -0
  72. data/spec/responses/relation/open_corporates_spec.rb +31 -0
  73. data/spec/responses/relation/open_oil_spec.rb +31 -0
  74. data/spec/result_spec.rb +90 -0
  75. data/spec/spec_helper.rb +21 -0
  76. data/spec/support/shared_examples_for_requests.rb +155 -0
  77. data/spec/validator_spec.rb +43 -0
  78. data/whos_got_dirt.gemspec +28 -0
  79. metadata +281 -0
@@ -0,0 +1,66 @@
1
+ module WhosGotDirt
2
+ module Responses
3
+ module Entity
4
+ # Converts entities from the LittleSis API to Popolo format.
5
+ #
6
+ # @see http://api.littlesis.org/documentation
7
+ class LittleSis < Helpers::LittleSisHelper
8
+ @count_field = 'Entities'
9
+
10
+ @template = {
11
+ '@type' => 'Entity',
12
+ 'type' => '/primary_type',
13
+ 'name' => '/name',
14
+ 'description' => '/description',
15
+ 'identifiers' => [{
16
+ 'identifier' => '/id',
17
+ 'scheme' => 'LittleSis',
18
+ }],
19
+ 'links' => [{
20
+ 'url' => '/website',
21
+ 'note' => 'Website',
22
+ }, {
23
+ 'url' => '/uri',
24
+ 'note' => 'LittleSis page',
25
+ }, {
26
+ 'url' => '/api_uri',
27
+ 'note' => 'LittleSis API detail',
28
+ }],
29
+ 'updated_at' => '/updated_at',
30
+
31
+ # Class-specific.
32
+ 'start_date' => lambda{|data|
33
+ if JsonPointer.new(data, '/primary_type').value == 'Person'
34
+ k = 'birth_date'
35
+ else # 'Org'
36
+ k = 'founding_date'
37
+ end
38
+ [k, JsonPointer.new(data, '/start_date').value]
39
+ },
40
+ 'end_date' => lambda{|data|
41
+ if JsonPointer.new(data, '/primary_type').value == 'Person'
42
+ k = 'death_date'
43
+ else # 'Org'
44
+ k = 'dissolution_date'
45
+ end
46
+ [k, JsonPointer.new(data, '/end_date').value]
47
+ },
48
+ 'parent_id' => '/parent_id',
49
+
50
+ # API-specific.
51
+ 'is_current' => '/is_current',
52
+ 'summary' => '/summary',
53
+ }
54
+
55
+ # Transforms the parsed response body into results.
56
+ #
57
+ # @return [Array<Hash>] the results
58
+ def to_a
59
+ parsed_body['Data']['Entities']['Entity'].map do |data|
60
+ Result.new('Entity', renderer.result(data), self).finalize!
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,61 @@
1
+ module WhosGotDirt
2
+ module Responses
3
+ module Entity
4
+ # Converts companies from the OpenCorporates API to Popolo format.
5
+ #
6
+ # @see https://api.opencorporates.com/documentation/API-Reference
7
+ class OpenCorporates < Helpers::OpenCorporatesHelper
8
+ @template = {
9
+ '@type' => 'Entity',
10
+ 'name' => '/name',
11
+ 'classification' => '/company_type',
12
+ 'founding_date' => '/incorporation_date',
13
+ 'dissolution_date' => '/dissolution_date',
14
+ 'created_at' => '/created_at',
15
+ 'updated_at' => '/updated_at',
16
+ 'other_names' => [{
17
+ 'name' => '/previous_names/company_name',
18
+ 'start_date' => '/previous_names/start_date',
19
+ 'end_date' => '/previous_names/end_date',
20
+ 'note' => '/previous_names/type',
21
+ }],
22
+ 'identifiers' => [{
23
+ 'identifier' => '/company_number',
24
+ 'scheme' => 'Company Register',
25
+ }],
26
+ 'contact_details' => [{
27
+ 'type' => 'address',
28
+ 'value' => '/registered_address_in_full',
29
+ }],
30
+ 'links' => [{
31
+ 'url' => '/opencorporates_url',
32
+ 'note' => 'OpenCorporates page',
33
+ }, {
34
+ 'url' => '/registry_url',
35
+ 'note' => 'Register page',
36
+ }],
37
+ 'sources' => [{
38
+ 'url' => '/sources/url',
39
+ 'note' => '/sources/publisher',
40
+ 'retrieved_at' => '/sources/retrieved_at', # @todo check Dublin Core, etc.
41
+ }],
42
+ # API-specific.
43
+ 'branch_status' => '/branch_status', # @todo if boolean, use "branch" as in request
44
+ 'current_status' => '/current_status',
45
+ 'inactive' => '/inactive',
46
+ 'jurisdiction_code' => '/jurisdiction_code',
47
+ 'retrieved_at' => '/retrieved_at', # @todo check Dublin Core, etc.
48
+ }
49
+
50
+ # Transforms the parsed response body into results.
51
+ #
52
+ # @return [Array<Hash>] the results
53
+ def to_a
54
+ parsed_body['companies'].map do |data|
55
+ Result.new('Organization', renderer.result(data['company']), self).finalize!
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,42 @@
1
+ module WhosGotDirt
2
+ module Responses
3
+ module Entity
4
+ # Converts entities from the OpenDuka API to Popolo format.
5
+ #
6
+ # @see http://www.openduka.org/index.php/api/documentation
7
+ class OpenDuka < Response
8
+ @template = {
9
+ '@type' => 'Entity',
10
+ 'name' => '/Name',
11
+ 'identifiers' => [{
12
+ 'identifier' => '/ID',
13
+ 'scheme' => 'OpenDuka',
14
+ }],
15
+ }
16
+
17
+ # Parses the response body.
18
+ #
19
+ # @return [Array<Hash>] the parsed response body
20
+ def parse_body
21
+ JSON.load(body)
22
+ end
23
+
24
+ # Returns the total number of matching results.
25
+ #
26
+ # @return [Fixnum] the total number of matching results
27
+ def count
28
+ parsed_body.size
29
+ end
30
+
31
+ # Transforms the parsed response body into results.
32
+ #
33
+ # @return [Array<Hash>] the results
34
+ def to_a
35
+ parsed_body.map do |data|
36
+ Result.new('Entity', renderer.result(data), self).finalize!
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,44 @@
1
+ module WhosGotDirt
2
+ module Responses
3
+ module Entity
4
+ # Converts entities from the Poderopedia API to Popolo format.
5
+ #
6
+ # @see http://api.poderopedia.org/search
7
+ class Poderopedia < Response
8
+ @template = {
9
+ '@type' => 'Entity',
10
+ 'name' => '/alias',
11
+ 'identifiers' => [{
12
+ 'identifier' => '/id',
13
+ 'scheme' => 'Poderopedia',
14
+ }],
15
+ 'description' => '/shortBio'
16
+ }
17
+
18
+ # Parses the response body.
19
+ #
20
+ # @return [Array<Hash>] the parsed response body
21
+ def parse_body
22
+ parsed = JSON.load(body)
23
+ parsed['organization'] || parsed['person']
24
+ end
25
+
26
+ # Returns the total number of matching results.
27
+ #
28
+ # @return [Fixnum] the total number of matching results
29
+ def count
30
+ parsed_body.size
31
+ end
32
+
33
+ # Transforms the parsed response body into results.
34
+ #
35
+ # @return [Array<Hash>] the results
36
+ def to_a
37
+ parsed_body.map do |data|
38
+ Result.new('Entity', renderer.result(data), self).finalize!
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,7 @@
1
+ module WhosGotDirt
2
+ module Responses
3
+ # Responses for entities.
4
+ module Entity
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,34 @@
1
+ module WhosGotDirt
2
+ module Responses
3
+ module Helpers
4
+ class LittleSisHelper < Response
5
+ class << self
6
+ # @!attribute [r] count_field
7
+ # @return [Hash] the field storing the number of results
8
+ attr_reader :count_field
9
+ end
10
+
11
+ # Parses the response body.
12
+ #
13
+ # @return [Array<Hash>] the parsed response body
14
+ def parse_body
15
+ Nori.new.parse(body)['Response']
16
+ end
17
+
18
+ # Returns the total number of matching results.
19
+ #
20
+ # @return [Fixnum] the total number of matching results
21
+ def count
22
+ Integer(parsed_body['Meta']['TotalCount'] || parsed_body['Meta']['ResultCount'][self.class.count_field])
23
+ end
24
+
25
+ # Returns the current page number.
26
+ #
27
+ # @return [Fixnum] the current page number
28
+ def page
29
+ Integer(parsed_body['Meta']['Parameters']['page'] || 1)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,28 @@
1
+ module WhosGotDirt
2
+ module Responses
3
+ module Helpers
4
+ class OpenCorporatesHelper < Response
5
+ # Parses the response body.
6
+ #
7
+ # @return [Array<Hash>] the parsed response body
8
+ def parse_body
9
+ JSON.load(body)['results']
10
+ end
11
+
12
+ # Returns the total number of matching results.
13
+ #
14
+ # @return [Fixnum] the total number of matching results
15
+ def count
16
+ parsed_body['total_count']
17
+ end
18
+
19
+ # Returns the current page number.
20
+ #
21
+ # @return [Fixnum] the current page number
22
+ def page
23
+ parsed_body['page']
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,42 @@
1
+ module WhosGotDirt
2
+ module Responses
3
+ module List
4
+ # Converts lists from the OpenCorporates API to Popolo format.
5
+ #
6
+ # @see http://api.littlesis.org/documentation
7
+ class LittleSis < Helpers::LittleSisHelper
8
+ @count_field = 'Lists'
9
+
10
+ @template = {
11
+ '@type' => 'List',
12
+ 'name' => '/name',
13
+ 'description' => '/description',
14
+ 'number_of_items' => '/num_entities',
15
+ 'item_list_order' => lambda{|data|
16
+ v = JsonPointer.new(data, '/is_ranked').value
17
+ if v == '1'
18
+ v = 'ascending'
19
+ else
20
+ v = 'unordered'
21
+ end
22
+ ['item_list_order', v]
23
+ },
24
+ 'updated_at' => '/updated_at',
25
+ 'identifiers' => [{
26
+ 'identifier' => '/id',
27
+ 'scheme' => 'LittleSis',
28
+ }],
29
+ }
30
+
31
+ # Transforms the parsed response body into results.
32
+ #
33
+ # @return [Array<Hash>] the results
34
+ def to_a
35
+ parsed_body['Data']['Lists']['List'].map do |data|
36
+ Result.new('List', renderer.result(data), self).finalize!
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,34 @@
1
+ module WhosGotDirt
2
+ module Responses
3
+ module List
4
+ # Converts corporate groupings from the OpenCorporates API to Popolo format.
5
+ #
6
+ # @see https://api.opencorporates.com/documentation/API-Reference
7
+ class OpenCorporates < Helpers::OpenCorporatesHelper
8
+ @template = {
9
+ '@type' => 'List',
10
+ 'name' => '/name',
11
+ 'created_at' => '/created_at',
12
+ 'updated_at' => '/updated_at',
13
+ 'identifiers' => [{
14
+ 'identifier' => '/wikipedia_id',
15
+ 'scheme' => 'Wikipedia',
16
+ }],
17
+ 'links' => [{
18
+ 'url' => '/opencorporates_url',
19
+ 'note' => 'OpenCorporates page',
20
+ }],
21
+ }
22
+
23
+ # Transforms the parsed response body into results.
24
+ #
25
+ # @return [Array<Hash>] the results
26
+ def to_a
27
+ parsed_body['corporate_groupings'].map do |data|
28
+ Result.new('List', renderer.result(data['corporate_grouping']), self).finalize!
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,7 @@
1
+ module WhosGotDirt
2
+ module Responses
3
+ # Responses for lists.
4
+ module List
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,66 @@
1
+ module WhosGotDirt
2
+ module Responses
3
+ module Relation
4
+ # Converts corporate officerships from the OpenCorporates API to Popolo format.
5
+ #
6
+ # @see https://api.opencorporates.com/documentation/API-Reference
7
+ class OpenCorporates < Helpers::OpenCorporatesHelper
8
+ @template = {
9
+ '@type' => 'Relation',
10
+ 'subject' => [{
11
+ 'name' => '/name',
12
+ 'birth_date' => '/date_of_birth',
13
+ 'contact_details' => [{
14
+ 'type' => 'address',
15
+ 'value' => '/address',
16
+ }],
17
+ # API-specific.
18
+ 'nationality' => '/nationality',
19
+ 'occupation' => '/occupation',
20
+ }],
21
+ 'object' => {
22
+ 'name' => '/company/name',
23
+ 'identifiers' => [{
24
+ 'identifier' => '/company/company_number',
25
+ 'scheme' => 'Company Register',
26
+ }],
27
+ 'links' => [{
28
+ 'url' => '/company/opencorporates_url',
29
+ 'note' => 'OpenCorporates page',
30
+ }],
31
+ # API-specific.
32
+ 'jurisdiction_code' => '/company/jurisdiction_code',
33
+ },
34
+ 'start_date' => '/start_date',
35
+ 'end_date' => '/end_date',
36
+ 'identifiers' => [{
37
+ 'identifier' => '/id',
38
+ 'scheme' => 'OpenCorporates',
39
+ }, {
40
+ 'identifier' => '/uid',
41
+ 'scheme' => 'Company Register',
42
+ }],
43
+ 'links' => [{
44
+ 'url' => '/opencorporates_url',
45
+ 'note' => 'OpenCorporates page',
46
+ }],
47
+ 'updated_at' => '/retrieved_at',
48
+ # API-specific.
49
+ 'inactive' => '/inactive',
50
+ 'current_status' => '/current_status',
51
+ 'jurisdiction_code' => '/jurisdiction_code',
52
+ 'role' => '/position',
53
+ }
54
+
55
+ # Transforms the parsed response body into results.
56
+ #
57
+ # @return [Array<Hash>] the results
58
+ def to_a
59
+ parsed_body['officers'].map do |data|
60
+ Result.new('Relation', renderer.result(data['officer']), self).finalize!
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,70 @@
1
+ module WhosGotDirt
2
+ module Responses
3
+ module Relation
4
+ # Converts concessions from the OpenOil API to Popolo format.
5
+ #
6
+ # @see http://openoil.net/openoil-api/
7
+ class OpenOil < Response
8
+ @template = {
9
+ '@type' => 'Relation',
10
+ 'subject' => lambda{|data|
11
+ v = JsonPointer.new(data, '/licensees').value
12
+ ['subject', v.map{|licensee| {'name' => licensee}}]
13
+ },
14
+ 'identifiers' => [{
15
+ 'identifier' => '/identifier',
16
+ 'scheme' => 'OpenOil',
17
+ }],
18
+ 'links' => [{
19
+ 'url' => '/url_api',
20
+ 'note' => 'OpenOil API detail',
21
+ }, {
22
+ 'url' => '/url_wiki',
23
+ 'note' => 'OpenOil wiki page',
24
+ }],
25
+ 'name' => '/name',
26
+ 'created_at' => '/source_date',
27
+ 'updated_at' => '/retrieved_date',
28
+ 'sources' => [{
29
+ 'url' => '/source_document',
30
+ }],
31
+ # API-specific.
32
+ 'additional_properties' => '/details',
33
+ 'country_code' => '/country',
34
+ 'status' => '/status',
35
+ 'type' => '/type',
36
+ }
37
+
38
+ # Parses the response body.
39
+ #
40
+ # @return [Array<Hash>] the parsed response body
41
+ def parse_body
42
+ JSON.load(body)
43
+ end
44
+
45
+ # Returns the total number of matching results.
46
+ #
47
+ # @return [Fixnum] the total number of matching results
48
+ def count
49
+ parsed_body['result_count']
50
+ end
51
+
52
+ # Returns the current page number.
53
+ #
54
+ # @return [Fixnum] the current page number
55
+ def page
56
+ parsed_body['page']
57
+ end
58
+
59
+ # Transforms the parsed response body into results.
60
+ #
61
+ # @return [Array<Hash>] the results
62
+ def to_a
63
+ parsed_body['results'].map do |data|
64
+ Result.new('Relation', renderer.result(data), self).finalize!
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,7 @@
1
+ module WhosGotDirt
2
+ module Responses
3
+ # Responses for relations.
4
+ module Relation
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,106 @@
1
+ module WhosGotDirt
2
+ # A result from a response.
3
+ class Result
4
+ # @!attribute [r] result
5
+ # @return [Hash] the result
6
+ attr_reader :result
7
+
8
+ # @!attribute [r] response
9
+ # @return [Response] the response from which the result was created
10
+ attr_reader :response
11
+
12
+ # @!attribute [r] type
13
+ # @return [Response] the Popolo class to validate against
14
+ attr_reader :type
15
+
16
+ # Sets the result and response.
17
+ #
18
+ # @param [String] type the Popolo class to validate against
19
+ # @param [Hash] result the rendered result
20
+ # @param [Response] response the response from which the result was created
21
+ def initialize(type, result, response)
22
+ @type = type
23
+ @result = result
24
+ @response = response
25
+ end
26
+
27
+ # Adds the requested URL as a source.
28
+ #
29
+ # @return [Hash] the result
30
+ def add_source!
31
+ result['sources'] ||= []
32
+ result['sources'] << {
33
+ 'url' => response.env.url.to_s,
34
+ 'note' => response.class.name.rpartition('::')[2],
35
+ }
36
+ result
37
+ end
38
+
39
+ # Validates the result against its schema. If recognized errors occur, they
40
+ # are corrected, and the result is revalidated; if any error occurs during
41
+ # revalidation, an exception is raised.
42
+ #
43
+ # @param [Hash] opts options
44
+ # @option opts [Boolean] :strict (false) whether to raise an error if any
45
+ # error occurs
46
+ # @return [Hash] the result
47
+ # @raise if an unrecognized error occurs, or if any error occurs during
48
+ # revalidation
49
+ def validate!(opts = {})
50
+ # The code assumes that processing errors in reverse avoids re-indexing
51
+ # issues when deleting items from arrays. If this assumption is invalid,
52
+ # we can delete items on a time and re-validate using this code skeleton:
53
+ #
54
+ # begin
55
+ # Validator.validate(result, type)
56
+ # rescue JSON::Schema::ValidationError => e
57
+ # error = e.to_hash
58
+ # case error[:failed_attribute]
59
+ # when 'Properties'
60
+ # ...
61
+ # validate!
62
+ # ...
63
+ # end
64
+ # end
65
+
66
+ errors = Validator.validate(result, type)
67
+
68
+ if opts[:strict] && errors.any?
69
+ raise ValidationError.new(errors * '\n')
70
+ end
71
+
72
+ errors.reverse.each do |error|
73
+ pointer = JsonPointer.new(result, error[:fragment][1..-1])
74
+
75
+ case error.fetch(:failed_attribute)
76
+ when 'Properties'
77
+ # The property did not contain a required property. This should be due
78
+ # to the source having a null value.
79
+ pointer.delete
80
+ when 'Type'
81
+ # The property did not match one or more types. This should be due to
82
+ # the source having an integer instead of string value.
83
+ pointer.value = pointer.value.to_s
84
+ else
85
+ raise ValidationError.new(error.fetch(:message))
86
+ end
87
+ end
88
+
89
+ if errors.any?
90
+ validate!(strict: true)
91
+ end
92
+
93
+ result
94
+ end
95
+
96
+ # Adds the requested URL as a source, validates the result against its
97
+ # schema, and returns the updated result.
98
+ #
99
+ # @return [Hash] the result
100
+ def finalize!
101
+ add_source!
102
+ validate!
103
+ result
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,52 @@
1
+ module WhosGotDirt
2
+ module Validator
3
+ class << self
4
+ # Validates the data against the named schema and returns any errors.
5
+ #
6
+ # @param [Hash] data the data to validate
7
+ # @param [String] name the name of the definition in the JSON Schema
8
+ # @return [Array<ValidationError>] a list of validation errors
9
+ def validate(data, name)
10
+ validator = validators[name.downcase]
11
+ # @see https://github.com/ruby-json-schema/json-schema/blob/fa316dc9d39b922935aed8ec9fa0e4139b724ef5/lib/json-schema/validator.rb#L40
12
+ validator.instance_variable_set('@errors', [])
13
+ # `JSON::Validator#initialize_data` does nothing, in our case.
14
+ # `@@original_data` doesn't need to be re-initialized.
15
+ # @see https://github.com/ruby-json-schema/json-schema/blob/fa316dc9d39b922935aed8ec9fa0e4139b724ef5/lib/json-schema/validator.rb#L53
16
+ validator.instance_variable_set('@data', data)
17
+ validator.validate
18
+ end
19
+
20
+ private
21
+
22
+ # The json-schema gem is very, very slow, so we implement optimizations.
23
+
24
+ # Memoizes and returns validators using a fragment of the schema.
25
+ def validators
26
+ @validators ||= Hash.new do |hash,name|
27
+ v = validator.dup
28
+ # @see https://github.com/ruby-json-schema/json-schema/blob/fa316dc9d39b922935aed8ec9fa0e4139b724ef5/lib/json-schema/validator.rb#L73
29
+ v.instance_variable_set('@base_schema', v.schema_from_fragment(v.instance_variable_get('@base_schema'), "#/definitions/#{name}"))
30
+ hash[name] = v
31
+ end
32
+ end
33
+
34
+ # Memoizes a validator using the schema.
35
+ def validator
36
+ # `JSON::Validator#initialize_schema` runs faster if given a `Hash`.
37
+ @@validator ||= JSON::Validator.new(JSON.load(File.read(File.expand_path(File.join('..', '..', '..', 'schemas', 'popolo.json'), __FILE__))), {}, {
38
+ # Keep the cache - whatever it is.
39
+ clear_cache: false,
40
+ # It's safe to skip data parsing if the data is a `Hash`.
41
+ parse_data: false,
42
+ # Push errors onto `@errors` instead of raising. Setting to false would
43
+ # result in a single error being reported.
44
+ record_errors: true,
45
+ # `ValidationError#to_hash` is probably slower than
46
+ # `ValidationError#to_string`, but it is not yet a bottleneck.
47
+ errors_as_objects: true,
48
+ })
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,3 @@
1
+ module WhosGotDirt
2
+ VERSION = "0.0.2"
3
+ end