statbank_denmark 0.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.
- checksums.yaml +7 -0
- data/CHANGELOG.txt +9 -0
- data/Gemfile +3 -0
- data/README.md +127 -0
- data/lib/Object/inQ.rb +15 -0
- data/lib/StatBankDenmark/Client.rb +199 -0
- data/lib/StatBankDenmark/Error.rb +35 -0
- data/lib/StatBankDenmark/VERSION.rb +6 -0
- data/lib/StatBankDenmark.rb +35 -0
- data/lib/statbank_denmark.rb +3 -0
- data/statbank_denmark.gemspec +36 -0
- data/test/StatBankDenmark/Client_test.rb +316 -0
- data/test/StatBankDenmark/Error_test.rb +45 -0
- data/test/StatBankDenmark/VERSION_test.rb +9 -0
- data/test/StatBankDenmark_test.rb +15 -0
- data/test/helper.rb +15 -0
- data/test/integration_test.rb +40 -0
- metadata +140 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ff97514ce9d09ec1296d7ce144c5a9d104f427c7f5a0ef888d0728779bcc3195
|
4
|
+
data.tar.gz: f670282617270e9c6f7d7d826c80728852266e7d13920d4ac585d29af7c6d71c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 383718b874f4b6216078e3588788bab94a9723bdcf37fe35116a05e9cfefd849dadc5513a07a9a8fc921797db0e76c2967273793a517f4a08f7b1c5031552da3
|
7
|
+
data.tar.gz: e9b17ec3220fa02870fcb5d80d086507f901631042062cb89a3be5a02ae6fe10360db57ab96e4dc350eed96391933c5e748abe275b3f5dd5691b7ef5238f2336
|
data/CHANGELOG.txt
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
# statbank_denmark
|
2
|
+
|
3
|
+
A Ruby client for easy access to StatBank; Denmark's official statistics (Danmarks Statistik) REST API.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'statbank_denmark'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
```
|
16
|
+
$ bundle install
|
17
|
+
```
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
```
|
22
|
+
$ gem install statbank_denmark
|
23
|
+
```
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
### Defaults for all interfaces
|
28
|
+
```ruby
|
29
|
+
lang: 'en' # also can be 'da'
|
30
|
+
format: 'json' # except for data(), where it defaults to format: 'csv' and JSON data requires format: 'jsonstat'
|
31
|
+
```
|
32
|
+
|
33
|
+
### Via module methods
|
34
|
+
```ruby
|
35
|
+
# Get all subjects in English
|
36
|
+
StatBankDenmark.subjects # implicitly lang: 'en'
|
37
|
+
StatBankDenmark.subjects(lang: 'en') # explicitly lang: 'en'
|
38
|
+
|
39
|
+
# Get all subjects in Danish
|
40
|
+
StatBankDenmark.subjects(lang: 'da')
|
41
|
+
|
42
|
+
# Get subjects recursively
|
43
|
+
StatBankDenmark.subjects(recursive: true)
|
44
|
+
|
45
|
+
# Get a list of all tables
|
46
|
+
StatBankDenmark.tables
|
47
|
+
|
48
|
+
# Get a list of tables about a specific subject by code
|
49
|
+
StatBankDenmark.tables(subject: 4)
|
50
|
+
|
51
|
+
# Get a list of tables which are inactive
|
52
|
+
StatBankDenmark.tables(include_inactive: true)
|
53
|
+
|
54
|
+
# Get a list of tables which are inactive on a specific subject by code
|
55
|
+
StatBankDenmark.tables(include_inactive: true, subject: 4)
|
56
|
+
|
57
|
+
# Get info on a specific table
|
58
|
+
StatBankDenmark.table_info('STRAF42') # implicitly format: 'json'
|
59
|
+
StatBankDenmark.table_info('STRAF42', format: 'json') # explicitly format: 'json'
|
60
|
+
StatBankDenmark.table_info('STRAF42', format: 'csv') # format: 'csv'
|
61
|
+
|
62
|
+
# Get data from a specific table
|
63
|
+
StatBankDenmark.data('STRAF42') # implicitly format: 'csv'
|
64
|
+
StatBankDenmark.data('STRAF42', format: 'csv') # explicitly format: 'csv'
|
65
|
+
StatBankDenmark.data('STRAF42', format: 'jsonstat') # Returns json and must be specified with /jsonstat/i, not /json/i.
|
66
|
+
|
67
|
+
# Search tables for metadata strings
|
68
|
+
StatBankDenmark.search('population')
|
69
|
+
StatBankDenmark.search('POPULATION') # case insensitive search
|
70
|
+
StatBankDenmark.search('befolkning', lang: 'da') # lang: 'da'
|
71
|
+
StatBankDenmark.search('BEFOLKNING', lang: 'da') # case insensitive search and lang: 'da'
|
72
|
+
```
|
73
|
+
|
74
|
+
### Via instantiation
|
75
|
+
```ruby
|
76
|
+
client = StatBankDenmark.new
|
77
|
+
|
78
|
+
# Get all subjects in English
|
79
|
+
client.subjects # implicitly lang: 'en'
|
80
|
+
client.subjects(lang: 'en') # explicitly lang: 'en'
|
81
|
+
|
82
|
+
# Get all subjects in Danish
|
83
|
+
client.subjects(lang: 'da')
|
84
|
+
|
85
|
+
# Get subjects recursively
|
86
|
+
client.subjects(recursive: true)
|
87
|
+
|
88
|
+
# Get a list of all tables
|
89
|
+
client.tables
|
90
|
+
|
91
|
+
# Get a list of tables about a specific subject by code
|
92
|
+
client.tables(subject: 4)
|
93
|
+
|
94
|
+
# Get a list of tables which are inactive
|
95
|
+
client.tables(include_inactive: true)
|
96
|
+
|
97
|
+
# Get a list of tables which are inactive on a specific subject by code
|
98
|
+
client.tables(include_inactive: true, subject: 4)
|
99
|
+
|
100
|
+
# Get info on a specific table
|
101
|
+
client.table_info('STRAF42') # implicitly format: 'json'
|
102
|
+
client.table_info('STRAF42', format: 'json') # explicitly format: 'json'
|
103
|
+
client.table_info('STRAF42', format: 'csv') # format: 'csv'
|
104
|
+
|
105
|
+
# Get data from a specific table
|
106
|
+
client.data('STRAF42') # implicitly format: 'csv'
|
107
|
+
client.data('STRAF42', format: 'csv') # explicitly format: 'csv'
|
108
|
+
client.data('STRAF42', format: 'jsonstat') # Returns json and must be specified with /jsonstat/i, not /json/i.
|
109
|
+
|
110
|
+
# Search tables for metadata strings
|
111
|
+
client.search('population')
|
112
|
+
client.search('POPULATION') # case insensitive search
|
113
|
+
client.search('befolkning', lang: 'da') # lang: 'da'
|
114
|
+
client.search('BEFOLKNING', lang: 'da') # case insensitive search and lang: 'da'
|
115
|
+
```
|
116
|
+
|
117
|
+
## Contributing
|
118
|
+
|
119
|
+
1. Fork it (https://github.com/thoran/statbank_denmark/fork)
|
120
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
121
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
122
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
123
|
+
5. Create a new pull request
|
124
|
+
|
125
|
+
## License
|
126
|
+
|
127
|
+
The gem is available as open source under the terms of the [Ruby License](https://opensource.org/licenses/MIT).
|
data/lib/Object/inQ.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# Object/inQ.rb
|
2
|
+
# Object#in?
|
3
|
+
|
4
|
+
# 20250906
|
5
|
+
# 0.5.1
|
6
|
+
|
7
|
+
# Description: This is essentially the reverse of Enumerable#include?. It can be pointed at any object and takes a collection as an argument.
|
8
|
+
|
9
|
+
class Object
|
10
|
+
|
11
|
+
def in?(*a)
|
12
|
+
a.flatten.include?(self)
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
# lib/StatBankDenmark/Client.rb
|
2
|
+
# StatBankDenmark::Client
|
3
|
+
|
4
|
+
gem 'http.rb'; require 'http.rb'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
require_relative './Error'
|
8
|
+
require_relative '../Object/inQ'
|
9
|
+
|
10
|
+
module StatBankDenmark
|
11
|
+
class Client
|
12
|
+
ALLOWABLE_VERBS = %w{GET POST}
|
13
|
+
API_HOST = 'api.statbank.dk'
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def path_prefix
|
17
|
+
'/v1'
|
18
|
+
end
|
19
|
+
end # class << self
|
20
|
+
|
21
|
+
def subjects(
|
22
|
+
format: 'json',
|
23
|
+
include_tables: nil,
|
24
|
+
lang: 'en',
|
25
|
+
omit_empty: nil,
|
26
|
+
recursive: nil
|
27
|
+
)
|
28
|
+
response = get(
|
29
|
+
path: '/subjects',
|
30
|
+
args: {
|
31
|
+
format: format,
|
32
|
+
includeTables: include_tables,
|
33
|
+
lang: lang,
|
34
|
+
omitEmptySubjects: omit_empty,
|
35
|
+
recursive: recursive,
|
36
|
+
}
|
37
|
+
)
|
38
|
+
handle_response(response)
|
39
|
+
end
|
40
|
+
|
41
|
+
def tables(
|
42
|
+
format: 'json',
|
43
|
+
include_inactive: nil,
|
44
|
+
lang: 'en',
|
45
|
+
subject: nil
|
46
|
+
)
|
47
|
+
response = get(
|
48
|
+
path: '/tables',
|
49
|
+
args: {
|
50
|
+
format: format,
|
51
|
+
includeInactive: include_inactive,
|
52
|
+
lang: lang,
|
53
|
+
subjects: subject,
|
54
|
+
}
|
55
|
+
)
|
56
|
+
handle_response(response)
|
57
|
+
end
|
58
|
+
|
59
|
+
def table_info(
|
60
|
+
table_id,
|
61
|
+
format: 'json',
|
62
|
+
lang: 'en'
|
63
|
+
)
|
64
|
+
raise ArgumentError, "table_id is required" if table_id.nil? || table_id.empty?
|
65
|
+
response = get(
|
66
|
+
path: '/tableinfo',
|
67
|
+
args: {
|
68
|
+
format: format,
|
69
|
+
lang: lang,
|
70
|
+
id: table_id,
|
71
|
+
}
|
72
|
+
)
|
73
|
+
handle_response(response)
|
74
|
+
end
|
75
|
+
|
76
|
+
def data(
|
77
|
+
table_id,
|
78
|
+
format: 'csv',
|
79
|
+
lang: 'en',
|
80
|
+
value_presentation: 'Default',
|
81
|
+
variables: {}
|
82
|
+
)
|
83
|
+
raise ArgumentError, "table_id is required" if table_id.nil? || table_id.empty?
|
84
|
+
body = {
|
85
|
+
table: table_id,
|
86
|
+
lang: lang,
|
87
|
+
format: format,
|
88
|
+
valuePresentation: value_presentation,
|
89
|
+
}
|
90
|
+
unless variables.empty?
|
91
|
+
body[:variables] = variables.map do |var_name, values|
|
92
|
+
{
|
93
|
+
code: var_name.to_s,
|
94
|
+
values: Array(values)
|
95
|
+
}
|
96
|
+
end
|
97
|
+
end
|
98
|
+
response = post(path: '/data', args: body)
|
99
|
+
handle_response(response)
|
100
|
+
end
|
101
|
+
|
102
|
+
def search(query, lang: 'en')
|
103
|
+
all_tables = tables(lang: lang)
|
104
|
+
return [] unless all_tables.is_a?(Array)
|
105
|
+
all_tables.select do |table|
|
106
|
+
text = table['text'] || table[:text] || ''
|
107
|
+
text.downcase.include?(query.downcase)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def initialize(timeout: 30)
|
114
|
+
@timeout = timeout
|
115
|
+
end
|
116
|
+
|
117
|
+
def get(path:, args: {})
|
118
|
+
do_request(verb: 'GET', path: path, args: args, options: {open_timeout: @timeout, read_timeout: @timeout})
|
119
|
+
end
|
120
|
+
|
121
|
+
def post(path:, args: {})
|
122
|
+
do_request(verb: 'POST', path: path, args: args, options: {open_timeout: @timeout, read_timeout: @timeout})
|
123
|
+
end
|
124
|
+
|
125
|
+
def do_request(verb:, path:, args: {}, options: {})
|
126
|
+
verb = verb.to_s.upcase
|
127
|
+
raise ArgumentError, "Unsupported HTTP method: #{verb}" unless verb.in?(ALLOWABLE_VERBS)
|
128
|
+
request_string = request_string(path: path)
|
129
|
+
args = args.reject{|k,v| v.nil?}
|
130
|
+
headers = {'Content-Type' => 'application/json'}
|
131
|
+
HTTP.send(verb.downcase, request_string, args, headers, options)
|
132
|
+
end
|
133
|
+
|
134
|
+
def request_string(path:)
|
135
|
+
"https://#{API_HOST}#{self.class.path_prefix}#{path}"
|
136
|
+
end
|
137
|
+
|
138
|
+
def handle_response(response)
|
139
|
+
if response.success?
|
140
|
+
body = response.body.to_s
|
141
|
+
parsed_response(response)
|
142
|
+
else
|
143
|
+
case response.code.to_i
|
144
|
+
when 400
|
145
|
+
raise BadRequestError.new(
|
146
|
+
code: response.code,
|
147
|
+
message: response.message,
|
148
|
+
body: response.body
|
149
|
+
)
|
150
|
+
when 404
|
151
|
+
raise NotFoundError.new(
|
152
|
+
code: response.code,
|
153
|
+
message: response.message,
|
154
|
+
body: response.body
|
155
|
+
)
|
156
|
+
when 429
|
157
|
+
raise RateLimitError.new(
|
158
|
+
code: response.code,
|
159
|
+
message: response.message,
|
160
|
+
body: response.body
|
161
|
+
)
|
162
|
+
when 500..599
|
163
|
+
raise ServerError.new(
|
164
|
+
code: response.code,
|
165
|
+
message: response.message,
|
166
|
+
body: response.body
|
167
|
+
)
|
168
|
+
else
|
169
|
+
raise APIError.new(
|
170
|
+
code: response.code,
|
171
|
+
message: response.message,
|
172
|
+
body: response.body
|
173
|
+
)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def parsed_response(response)
|
179
|
+
case response['content-type']
|
180
|
+
when /application\/json/
|
181
|
+
begin
|
182
|
+
JSON.parse(response.body)
|
183
|
+
rescue JSON::ParserError
|
184
|
+
response.body
|
185
|
+
end
|
186
|
+
when /text\/csv/
|
187
|
+
response.body
|
188
|
+
when /application\/xml/, /text\/xml/
|
189
|
+
response.body
|
190
|
+
else
|
191
|
+
begin
|
192
|
+
JSON.parse(response.body)
|
193
|
+
rescue JSON::ParserError
|
194
|
+
response.body
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# lib/StatBankDenmark/Error.rb
|
2
|
+
# StatBankDenmark::Error
|
3
|
+
|
4
|
+
module StatBankDenmark
|
5
|
+
class Error < RuntimeError
|
6
|
+
attr_reader\
|
7
|
+
:code,
|
8
|
+
:message,
|
9
|
+
:body
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
"#{self.class} (#{@code}): #{@message}"
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def initialize(code:, message:, body:)
|
18
|
+
@code = code
|
19
|
+
@message = message
|
20
|
+
@body = body
|
21
|
+
super(message)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# 400
|
26
|
+
class BadRequestError < Error; end
|
27
|
+
# 404
|
28
|
+
class NotFoundError < Error; end
|
29
|
+
# 429
|
30
|
+
class RateLimitError < Error; end
|
31
|
+
# 500..599
|
32
|
+
class ServerError < Error; end
|
33
|
+
# Everything else.
|
34
|
+
class APIError < Error; end
|
35
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# lib/StatBankDenmark.rb
|
2
|
+
# StatBankDenmark
|
3
|
+
|
4
|
+
require_relative './StatBankDenmark/VERSION'
|
5
|
+
require_relative './StatBankDenmark/Client'
|
6
|
+
|
7
|
+
module StatBankDenmark
|
8
|
+
class << self
|
9
|
+
def subjects(**options)
|
10
|
+
client.subjects(**options)
|
11
|
+
end
|
12
|
+
|
13
|
+
def tables(**options)
|
14
|
+
client.tables(**options)
|
15
|
+
end
|
16
|
+
|
17
|
+
def table_info(table_id, **options)
|
18
|
+
client.table_info(table_id, **options)
|
19
|
+
end
|
20
|
+
|
21
|
+
def data(table_id, **options)
|
22
|
+
client.data(table_id, **options)
|
23
|
+
end
|
24
|
+
|
25
|
+
def search(query, **options)
|
26
|
+
client.search(query, **options)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def client
|
32
|
+
@client ||= Client.new
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require_relative './lib/StatBankDenmark/VERSION'
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = 'statbank_denmark'
|
5
|
+
|
6
|
+
spec.version = StatBankDenmark::VERSION
|
7
|
+
spec.date = '2025-09-09'
|
8
|
+
|
9
|
+
spec.summary = "A Ruby client for the StatBank Denmark API."
|
10
|
+
spec.description = "A Ruby client for easy access to StatBank; Denmark's official statistics (Danmarks Statistik) REST API."
|
11
|
+
|
12
|
+
spec.authors = 'thoran'
|
13
|
+
spec.email = 'code@thoran.com'
|
14
|
+
spec.homepage = 'https://github.com/thoran/statbank_denmark'
|
15
|
+
spec.license = 'Ruby'
|
16
|
+
|
17
|
+
spec.required_ruby_version = '>= 2.7'
|
18
|
+
|
19
|
+
spec.files = [
|
20
|
+
'CHANGELOG.txt',
|
21
|
+
'Gemfile',
|
22
|
+
'README.md',
|
23
|
+
'statbank_denmark.gemspec',
|
24
|
+
Dir['lib/**/*.rb'],
|
25
|
+
Dir['test/**/*.rb']
|
26
|
+
].flatten
|
27
|
+
spec.require_paths = ['lib']
|
28
|
+
|
29
|
+
spec.add_dependency('http.rb')
|
30
|
+
spec.add_dependency('json')
|
31
|
+
|
32
|
+
spec.add_development_dependency('minitest')
|
33
|
+
spec.add_development_dependency('minitest-spec-context')
|
34
|
+
spec.add_development_dependency('webmock')
|
35
|
+
spec.add_development_dependency('vcr')
|
36
|
+
end
|
@@ -0,0 +1,316 @@
|
|
1
|
+
# test/StatBankDenmark/Client_test.rb
|
2
|
+
|
3
|
+
require_relative '../helper'
|
4
|
+
|
5
|
+
describe StatBankDenmark::Client do
|
6
|
+
let(:client){StatBankDenmark::Client.new}
|
7
|
+
|
8
|
+
describe "#subjects" do
|
9
|
+
context "with English language" do
|
10
|
+
let(:result){client.subjects(lang: 'en')}
|
11
|
+
|
12
|
+
it "returns available subjects in English" do
|
13
|
+
VCR.use_cassette("subjects_en") do
|
14
|
+
_(result).must_be_kind_of(Array)
|
15
|
+
_(result).wont_be_empty
|
16
|
+
_(result.first.keys).must_include('id')
|
17
|
+
_(result.first.keys).must_include('description')
|
18
|
+
_(result.first.keys).must_include('active')
|
19
|
+
_(result.first.keys).must_include('hasSubjects')
|
20
|
+
_(result.first.keys).must_include('subjects')
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context "with Danish language" do
|
26
|
+
let(:result){client.subjects(lang: 'da')}
|
27
|
+
|
28
|
+
it "returns available subjects in Danish" do
|
29
|
+
VCR.use_cassette("subjects_da") do
|
30
|
+
_(result).must_be_kind_of(Array)
|
31
|
+
_(result).wont_be_empty
|
32
|
+
_(result.first.keys).must_include('id')
|
33
|
+
_(result.first.keys).must_include('description')
|
34
|
+
_(result.first.keys).must_include('active')
|
35
|
+
_(result.first.keys).must_include('hasSubjects')
|
36
|
+
_(result.first.keys).must_include('subjects')
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context "with recursive hierarchy enabled" do
|
42
|
+
let(:result){client.subjects(recursive: true)}
|
43
|
+
|
44
|
+
it "handles recursive parameter" do
|
45
|
+
VCR.use_cassette("subjects_recursive") do
|
46
|
+
_(result).must_be_kind_of(Array)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context "when API returns error" do
|
52
|
+
it "raises BadRequestError for invalid parameters" do
|
53
|
+
WebMock.stub_request(:get, /api\.statbank\.dk/).to_return(status: 400, body: 'Bad Request')
|
54
|
+
_(proc{client.subjects(lang: 'invalid')}).must_raise(StatBankDenmark::BadRequestError)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "#tables" do
|
60
|
+
context "when retrieving all tables" do
|
61
|
+
let(:result){client.tables}
|
62
|
+
|
63
|
+
it "returns all available tables" do
|
64
|
+
VCR.use_cassette("tables_all") do
|
65
|
+
_(result).must_be_kind_of(Array)
|
66
|
+
_(result).wont_be_empty
|
67
|
+
_(result.first).must_include('id')
|
68
|
+
_(result.first).must_include('text')
|
69
|
+
_(result.first).must_include('variables')
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
context "when filtering by specific subject" do
|
75
|
+
let(:result){client.tables(subject: 4)}
|
76
|
+
|
77
|
+
it "returns tables for crime and justice subject" do
|
78
|
+
VCR.use_cassette("tables_subject_crime") do
|
79
|
+
_(result).must_be_kind_of(Array)
|
80
|
+
_(result).wont_be_empty
|
81
|
+
crime_table = result.find{|t| t['id'] == 'STRAF42'}
|
82
|
+
_(crime_table).wont_be_nil if crime_table
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
context "when including inactive tables" do
|
88
|
+
let(:result){client.tables(include_inactive: true)}
|
89
|
+
|
90
|
+
it "handles include_inactive parameter" do
|
91
|
+
VCR.use_cassette("tables_include_inactive") do
|
92
|
+
_(result).must_be_kind_of(Array)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
it "raises NotFoundError for non-existent subject" do
|
98
|
+
WebMock.stub_request(:get, /api\.statbank\.dk/).to_return(status: 404, body: 'Not Found')
|
99
|
+
_(proc {client.tables(subject: '999')}).must_raise(StatBankDenmark::NotFoundError)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe "#table_info" do
|
104
|
+
context "when requesting metadata in English" do
|
105
|
+
let(:result){client.table_info('STRAF42')}
|
106
|
+
|
107
|
+
it "returns metadata for valid table" do
|
108
|
+
VCR.use_cassette("table_info_straf42") do
|
109
|
+
_(result).must_be_kind_of(Hash)
|
110
|
+
_(result).must_include('id')
|
111
|
+
_(result).must_include('text')
|
112
|
+
_(result).must_include('variables')
|
113
|
+
_(result['id']).must_equal('STRAF42')
|
114
|
+
|
115
|
+
variables = result['variables']
|
116
|
+
_(variables).must_be_kind_of(Array)
|
117
|
+
_(variables).wont_be_empty
|
118
|
+
_(variables.first).must_include('id')
|
119
|
+
_(variables.first).must_include('text')
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
context "when requesting metadata in Danish" do
|
125
|
+
let(:result){client.table_info('STRAF42', lang: 'da')}
|
126
|
+
|
127
|
+
it "returns metadata in Danish" do
|
128
|
+
VCR.use_cassette("table_info_straf42_da") do
|
129
|
+
_(result).must_be_kind_of(Hash)
|
130
|
+
_(result['id']).must_equal('STRAF42')
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
it "raises ArgumentError for nil table_id" do
|
136
|
+
_(proc{client.table_info(nil)}).must_raise(ArgumentError)
|
137
|
+
end
|
138
|
+
|
139
|
+
it "raises ArgumentError for empty table_id" do
|
140
|
+
_(proc{client.table_info('')}).must_raise(ArgumentError)
|
141
|
+
end
|
142
|
+
|
143
|
+
it "raises NotFoundError for non-existent table" do
|
144
|
+
WebMock.stub_request(:get, /api\.statbank\.dk/).to_return(status: 404, body: 'Table not found')
|
145
|
+
_(proc{client.table_info('NONEXISTENT')}).must_raise(StatBankDenmark::NotFoundError)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
describe "#data" do
|
150
|
+
context "when requesting CSV format" do
|
151
|
+
let(:result) do
|
152
|
+
client.data(
|
153
|
+
'STRAF42',
|
154
|
+
format: 'csv',
|
155
|
+
variables: {'HERKOMST1' => ['00'], 'TID' => ['2023']}
|
156
|
+
)
|
157
|
+
end
|
158
|
+
|
159
|
+
it "returns CSV data for table with variables" do
|
160
|
+
VCR.use_cassette("data_straf42_csv") do
|
161
|
+
_(result).must_be_kind_of(String)
|
162
|
+
_(result).must_include('HERKOMST1')
|
163
|
+
_(result).must_include('2023')
|
164
|
+
_(result.lines.length).must_be(:>, 1)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
context "when requesting JSON format" do
|
170
|
+
let(:result) do
|
171
|
+
client.data(
|
172
|
+
'STRAF42',
|
173
|
+
format: 'jsonstat',
|
174
|
+
variables: {'HERKOMST1' => ['00'], 'TID' => ['2023']}
|
175
|
+
)
|
176
|
+
end
|
177
|
+
|
178
|
+
it "returns JSON data for table" do
|
179
|
+
VCR.use_cassette("data_straf42_json") do
|
180
|
+
_(result).must_be_kind_of(Hash)
|
181
|
+
_(result).wont_be_empty
|
182
|
+
_(result['dataset']['dimension']).must_include('HERKOMST1')
|
183
|
+
_(result['dataset']['dimension']).must_include('ContentsCode')
|
184
|
+
_(result['dataset']['dimension']).must_include('Tid')
|
185
|
+
_(result['dataset']['dimension']).must_include('id')
|
186
|
+
_(result['dataset']['dimension']).must_include('size')
|
187
|
+
_(result['dataset']['dimension']).must_include('role')
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
context "when no variables specified" do
|
193
|
+
let(:result) do
|
194
|
+
client.data(
|
195
|
+
'STRAF42',
|
196
|
+
format: 'jsonstat'
|
197
|
+
)
|
198
|
+
end
|
199
|
+
|
200
|
+
it "returns all available data" do
|
201
|
+
VCR.use_cassette("data_straf42_all") do
|
202
|
+
_(result).must_be_kind_of(Hash)
|
203
|
+
_(result).wont_be_empty
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
it "raises ArgumentError for nil table_id" do
|
209
|
+
_(proc{client.data(nil)}).must_raise(ArgumentError)
|
210
|
+
end
|
211
|
+
|
212
|
+
it "raises ArgumentError for empty table_id" do
|
213
|
+
_(proc{client.data('')}).must_raise(ArgumentError)
|
214
|
+
end
|
215
|
+
|
216
|
+
it "raises BadRequestError for invalid variables" do
|
217
|
+
WebMock.stub_request(:post, /api\.statbank\.dk/).to_return(status: 400, body: 'Invalid variable specification')
|
218
|
+
_(proc{client.data('STRAF42', variables: {'INVALID' => ['bad']})}).must_raise(StatBankDenmark::BadRequestError)
|
219
|
+
end
|
220
|
+
|
221
|
+
it "handles rate limiting" do
|
222
|
+
WebMock.stub_request(:post, /api\.statbank\.dk/).to_return(status: 429, body: 'Rate limit exceeded')
|
223
|
+
_(proc{client.data('STRAF42')}).must_raise(StatBankDenmark::RateLimitError)
|
224
|
+
end
|
225
|
+
|
226
|
+
it "handles server errors" do
|
227
|
+
WebMock.stub_request(:post, /api\.statbank\.dk/).to_return(status: 500, body: 'Internal Server Error')
|
228
|
+
_(proc{client.data('STRAF42')}).must_raise(StatBankDenmark::ServerError)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
describe "#search" do
|
233
|
+
context "when searching for existing tables" do
|
234
|
+
let(:result){client.search('crime')}
|
235
|
+
|
236
|
+
it "finds tables by search term" do
|
237
|
+
VCR.use_cassette("search_crime") do
|
238
|
+
_(result).must_be_kind_of Array
|
239
|
+
crime_table = result.find{|t| t['id'] == 'STRAF42'}
|
240
|
+
_(crime_table).wont_be_nil if crime_table
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
context "when searching for non-existent tables" do
|
246
|
+
let(:result){client.search('xyzzyx_nonexistent_term')}
|
247
|
+
|
248
|
+
it "returns empty array for no matches" do
|
249
|
+
VCR.use_cassette("search_nomatch") do
|
250
|
+
_(result).must_be_kind_of(Array)
|
251
|
+
_(result).must_be_empty
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
context "when testing case sensitivity" do
|
257
|
+
let(:result_lower){client.search('population')}
|
258
|
+
let(:result_upper){client.search('POPULATION')}
|
259
|
+
|
260
|
+
it "is case insensitive" do
|
261
|
+
VCR.use_cassette("search_case_insensitive") do
|
262
|
+
_(result_lower).must_equal(result_upper)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
context "when searching in Danish" do
|
268
|
+
let(:result){client.search('befolkning', lang: 'da')}
|
269
|
+
|
270
|
+
it "searches in Danish language" do
|
271
|
+
VCR.use_cassette("search_danish") do
|
272
|
+
_(result).must_be_kind_of(Array)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
describe "error handling" do
|
279
|
+
context "when network times out" do
|
280
|
+
before do
|
281
|
+
WebMock.stub_request(:get, /api\.statbank\.dk/).to_timeout
|
282
|
+
end
|
283
|
+
|
284
|
+
it "handles network timeouts" do
|
285
|
+
_(proc{client.subjects}).must_raise(StandardError)
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
context "when API returns malformed JSON" do
|
290
|
+
let(:result){client.subjects}
|
291
|
+
|
292
|
+
before do
|
293
|
+
WebMock.stub_request(:get, /api\.statbank\.dk/).to_return(
|
294
|
+
status: 200,
|
295
|
+
headers: {'Content-Type' => 'application/json'},
|
296
|
+
body: 'invalid json{'
|
297
|
+
)
|
298
|
+
end
|
299
|
+
|
300
|
+
it "handles malformed JSON responses gracefully" do
|
301
|
+
_(result).must_equal('invalid json{')
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
describe "private methods" do
|
307
|
+
it "builds request URLs correctly" do
|
308
|
+
url = client.send(:request_string, path: '/subjects')
|
309
|
+
_(url).must_equal('https://api.statbank.dk/v1/subjects')
|
310
|
+
end
|
311
|
+
|
312
|
+
it "validates HTTP verbs" do
|
313
|
+
_(proc{client.send(:do_request, verb: 'PATCH', path: '/test')}).must_raise(ArgumentError)
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# test/StatBankDenmark/Error_test.rb
|
2
|
+
|
3
|
+
require_relative '../helper'
|
4
|
+
|
5
|
+
describe StatBankDenmark::Error do
|
6
|
+
context "" do
|
7
|
+
let(:error) do
|
8
|
+
StatBankDenmark::BadRequestError.new(
|
9
|
+
code: 400,
|
10
|
+
message: "Bad Request",
|
11
|
+
body: "Invalid parameter"
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "stores error details" do
|
16
|
+
_(error.code).must_equal(400)
|
17
|
+
_(error.message).must_equal("Bad Request")
|
18
|
+
_(error.body).must_equal("Invalid parameter")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
context "" do
|
23
|
+
let(:error) do
|
24
|
+
StatBankDenmark::NotFoundError.new(
|
25
|
+
code: 404,
|
26
|
+
message: "Not Found",
|
27
|
+
body: "Table does not exist"
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "provides meaningful string representation" do
|
32
|
+
_(error.to_s).must_equal("StatBankDenmark::NotFoundError (404): Not Found")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "error hierarchy" do
|
37
|
+
it "has correct inheritance" do
|
38
|
+
_(StatBankDenmark::BadRequestError).must_be :<, StatBankDenmark::Error
|
39
|
+
_(StatBankDenmark::NotFoundError).must_be :<, StatBankDenmark::Error
|
40
|
+
_(StatBankDenmark::RateLimitError).must_be :<, StatBankDenmark::Error
|
41
|
+
_(StatBankDenmark::ServerError).must_be :<, StatBankDenmark::Error
|
42
|
+
_(StatBankDenmark::APIError).must_be :<, StatBankDenmark::Error
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# test/StatBankDenmark_test.rb
|
2
|
+
|
3
|
+
require_relative './test_helper'
|
4
|
+
|
5
|
+
describe StatBankDenmark do
|
6
|
+
describe "module methods" do
|
7
|
+
it "responds to core API methods" do
|
8
|
+
_(StatBankDenmark).must_respond_to(:subjects)
|
9
|
+
_(StatBankDenmark).must_respond_to(:tables)
|
10
|
+
_(StatBankDenmark).must_respond_to(:table_info)
|
11
|
+
_(StatBankDenmark).must_respond_to(:data)
|
12
|
+
_(StatBankDenmark).must_respond_to(:search)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# test/helper.rb
|
2
|
+
|
3
|
+
require 'minitest/autorun'
|
4
|
+
require 'minitest-spec-context'
|
5
|
+
require 'webmock/minitest'
|
6
|
+
require 'vcr'
|
7
|
+
|
8
|
+
require_relative '../lib/statbank_denmark'
|
9
|
+
|
10
|
+
VCR.configure do |config|
|
11
|
+
config.cassette_library_dir = File.join(__dir__, 'fixtures', 'vcr_cassettes')
|
12
|
+
config.hook_into :webmock
|
13
|
+
config.default_cassette_options = {record: :once}
|
14
|
+
config.allow_http_connections_when_no_cassette = true
|
15
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# test/integration_test.rb
|
2
|
+
# Note: This test bypasses VCR and makes real API calls.
|
3
|
+
|
4
|
+
require_relative './helper'
|
5
|
+
|
6
|
+
describe "Integration Tests", :integration do
|
7
|
+
let(:client){StatBankDenmark::Client.new}
|
8
|
+
|
9
|
+
before do
|
10
|
+
skip unless ENV['RUN_INTEGRATION_TESTS']
|
11
|
+
end
|
12
|
+
|
13
|
+
it "can fetch real data from Danish Statistics API" do
|
14
|
+
# Test the API
|
15
|
+
subjects = client.subjects
|
16
|
+
_(subjects).must_be_kind_of(Array)
|
17
|
+
_(subjects).wont_be_empty
|
18
|
+
|
19
|
+
# Find People subject
|
20
|
+
people_subject = subjects.find{|s| s['description']&.downcase&.include?('people')}
|
21
|
+
skip "People subject not found" unless people_subject
|
22
|
+
|
23
|
+
# Get People tables
|
24
|
+
tables = client.tables(subject: people_subject['id'])
|
25
|
+
_(tables).must_be_kind_of(Array)
|
26
|
+
|
27
|
+
# Find FOLK1A table
|
28
|
+
folk1a = tables.find{|t| t['id'] == 'FOLK1A'}
|
29
|
+
skip "FOLK1A table not found" unless folk1a
|
30
|
+
|
31
|
+
# Get table metadata
|
32
|
+
info = client.table_info('FOLK1A')
|
33
|
+
_(info['id']).must_equal('FOLK1A')
|
34
|
+
|
35
|
+
# Get some actual data (limited to avoid large responses)
|
36
|
+
data = client.data('FOLK1A', format: 'jsonstat')
|
37
|
+
_(data).must_be_kind_of(Hash)
|
38
|
+
_(data).wont_be_empty
|
39
|
+
end
|
40
|
+
end
|
metadata
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: statbank_denmark
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- thoran
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-09-09 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: http.rb
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: json
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: minitest
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
type: :development
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: minitest-spec-context
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
type: :development
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: webmock
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
type: :development
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: vcr
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
type: :development
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
description: A Ruby client for easy access to StatBank; Denmark's official statistics
|
97
|
+
(Danmarks Statistik) REST API.
|
98
|
+
email: code@thoran.com
|
99
|
+
executables: []
|
100
|
+
extensions: []
|
101
|
+
extra_rdoc_files: []
|
102
|
+
files:
|
103
|
+
- CHANGELOG.txt
|
104
|
+
- Gemfile
|
105
|
+
- README.md
|
106
|
+
- lib/Object/inQ.rb
|
107
|
+
- lib/StatBankDenmark.rb
|
108
|
+
- lib/StatBankDenmark/Client.rb
|
109
|
+
- lib/StatBankDenmark/Error.rb
|
110
|
+
- lib/StatBankDenmark/VERSION.rb
|
111
|
+
- lib/statbank_denmark.rb
|
112
|
+
- statbank_denmark.gemspec
|
113
|
+
- test/StatBankDenmark/Client_test.rb
|
114
|
+
- test/StatBankDenmark/Error_test.rb
|
115
|
+
- test/StatBankDenmark/VERSION_test.rb
|
116
|
+
- test/StatBankDenmark_test.rb
|
117
|
+
- test/helper.rb
|
118
|
+
- test/integration_test.rb
|
119
|
+
homepage: https://github.com/thoran/statbank_denmark
|
120
|
+
licenses:
|
121
|
+
- Ruby
|
122
|
+
metadata: {}
|
123
|
+
rdoc_options: []
|
124
|
+
require_paths:
|
125
|
+
- lib
|
126
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '2.7'
|
131
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
132
|
+
requirements:
|
133
|
+
- - ">="
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: '0'
|
136
|
+
requirements: []
|
137
|
+
rubygems_version: 3.7.1
|
138
|
+
specification_version: 4
|
139
|
+
summary: A Ruby client for the StatBank Denmark API.
|
140
|
+
test_files: []
|