brreg_grunndata 0.1.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/.gitignore +10 -0
- data/.rspec +1 -0
- data/.rubocop.yml +8 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +90 -0
- data/Rakefile +8 -0
- data/bin/console +42 -0
- data/bin/setup +8 -0
- data/brreg_grunndata.gemspec +35 -0
- data/lib/brreg_grunndata.rb +11 -0
- data/lib/brreg_grunndata/client.rb +182 -0
- data/lib/brreg_grunndata/client/response.rb +91 -0
- data/lib/brreg_grunndata/client/response_header.rb +58 -0
- data/lib/brreg_grunndata/client/response_validator.rb +56 -0
- data/lib/brreg_grunndata/client/sok_enhet_query_to_xml.rb +82 -0
- data/lib/brreg_grunndata/configuration.rb +37 -0
- data/lib/brreg_grunndata/error.rb +6 -0
- data/lib/brreg_grunndata/service.rb +56 -0
- data/lib/brreg_grunndata/types/address.rb +17 -0
- data/lib/brreg_grunndata/types/base.rb +29 -0
- data/lib/brreg_grunndata/types/factories.rb +94 -0
- data/lib/brreg_grunndata/types/organization.rb +25 -0
- data/lib/brreg_grunndata/types/organizational_form.rb +12 -0
- data/lib/brreg_grunndata/utils.rb +96 -0
- data/lib/brreg_grunndata/version.rb +5 -0
- data/lib/brreg_grunndata/wsdl/grunndata.xml +575 -0
- metadata +171 -0
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
require_relative '../utils'
|
6
|
+
require_relative 'response_header'
|
7
|
+
|
8
|
+
module BrregGrunndata
|
9
|
+
class Client
|
10
|
+
# Wrapper around Savon's response
|
11
|
+
#
|
12
|
+
# Handles unwrapping of XML returned as string
|
13
|
+
class Response
|
14
|
+
using Utils::StringExt
|
15
|
+
|
16
|
+
extend Forwardable
|
17
|
+
def_delegators :@savon_response,
|
18
|
+
:soap_fault?, :http_error?,
|
19
|
+
:body
|
20
|
+
|
21
|
+
# Error raised whenever soap or http error occured so
|
22
|
+
# the success? is false but you are still asking for
|
23
|
+
# either header or message
|
24
|
+
class ResponseFailureError < Error; end
|
25
|
+
|
26
|
+
# Error raised when soap communication was a success,
|
27
|
+
# brreg response header indicates success, but for some
|
28
|
+
# reason we didn't get any return value as we expected
|
29
|
+
#
|
30
|
+
# Inspecting #header before calling #message will determin if this error
|
31
|
+
# is to be expected.
|
32
|
+
#
|
33
|
+
# @seehttps://www.brreg.no/produkter-og-tjenester/bestille/tilgang-til-enhetsregisteret-via-web-services/teknisk-beskrivelse-web-services/grunndataws/
|
34
|
+
class MessageEmptyError < Error; end
|
35
|
+
|
36
|
+
def initialize(savon_response)
|
37
|
+
@savon_response = savon_response
|
38
|
+
end
|
39
|
+
|
40
|
+
# Do we have a successful response?
|
41
|
+
#
|
42
|
+
# We do if:
|
43
|
+
# - We have no HTTP errors or SOAP faults. Savon handles this for us.
|
44
|
+
# - We have a response_header in the document returned from brreg
|
45
|
+
# which indicates success too. I don't know why BRREG needs this in
|
46
|
+
# addition to HTTP status codes and SOAP faults to communicate success
|
47
|
+
# or not :-(
|
48
|
+
def success?
|
49
|
+
@savon_response.success? && header.success?
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns the header from brreg's response
|
53
|
+
#
|
54
|
+
# The header contains the overall success of the SOAP operation.
|
55
|
+
# The header is something brreg's SOAP API has, so all in all we have
|
56
|
+
# http status, soap status and then this status located within the header :(
|
57
|
+
def header
|
58
|
+
raise ResponseFailureError unless @savon_response.success?
|
59
|
+
|
60
|
+
ResponseHeader.new response_grunndata[:response_header]
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns the header from brreg's response
|
64
|
+
#
|
65
|
+
# Keys are symbolized.
|
66
|
+
#
|
67
|
+
# @return Hash
|
68
|
+
def message
|
69
|
+
raise ResponseFailureError unless success?
|
70
|
+
|
71
|
+
@message ||= response_grunndata[:melding] || raise(MessageEmptyError)
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def response_grunndata
|
77
|
+
@response_grunndata ||= parse(body.values.first[:return])[:grunndata]
|
78
|
+
end
|
79
|
+
|
80
|
+
def parse(xml)
|
81
|
+
parser = Nori.new(
|
82
|
+
strip_namespaces: true,
|
83
|
+
advanced_typecasting: true,
|
84
|
+
convert_tags_to: ->(tag) { tag.underscore.to_sym }
|
85
|
+
)
|
86
|
+
|
87
|
+
parser.parse xml
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BrregGrunndata
|
4
|
+
class Client
|
5
|
+
# Represents a response header from brreg
|
6
|
+
#
|
7
|
+
# A response header has a main status and sub statuses.
|
8
|
+
#
|
9
|
+
# MAIN STATUS:
|
10
|
+
# 0 - OK
|
11
|
+
# 1 - OK, but some data is missing. See sub status for details
|
12
|
+
# -1 - An error as occured
|
13
|
+
#
|
14
|
+
# @see https://www.brreg.no/produkter-og-tjenester/bestille/tilgang-til-enhetsregisteret-via-web-services/teknisk-beskrivelse-web-services/grunndataws/
|
15
|
+
class ResponseHeader
|
16
|
+
MAIN_STATUS_SUCCESS_CODES = [0, 1].freeze
|
17
|
+
|
18
|
+
def initialize(nori_response_header)
|
19
|
+
@nori_response_header = nori_response_header
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns true if the brreg response header indicates success.
|
23
|
+
def success?
|
24
|
+
MAIN_STATUS_SUCCESS_CODES.include? main_status
|
25
|
+
end
|
26
|
+
|
27
|
+
def main_status
|
28
|
+
@main_status ||= cast_to_int(@nori_response_header[:hoved_status])
|
29
|
+
end
|
30
|
+
|
31
|
+
def sub_statuses
|
32
|
+
return [] unless @nori_response_header.key? :under_status
|
33
|
+
|
34
|
+
statuses = Array(@nori_response_header[:under_status][:under_status_melding])
|
35
|
+
|
36
|
+
@sub_statuses ||= statuses.map do |status|
|
37
|
+
{
|
38
|
+
code: cast_to_int(status.attributes['kode']),
|
39
|
+
message: status.to_s
|
40
|
+
}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# rubocop:disable Style/LineEndConcatenation
|
45
|
+
def inspect
|
46
|
+
"#<BrregGrunndata::ResponseHeader: main_status: #{main_status} " +
|
47
|
+
"sub_statuses: #{sub_statuses}>"
|
48
|
+
end
|
49
|
+
# rubocop:enable Style/LineEndConcatenation
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def cast_to_int(v)
|
54
|
+
Integer v, 10
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BrregGrunndata
|
4
|
+
class Client
|
5
|
+
class ResponseValidator
|
6
|
+
# Base class for all errors raised if header isn't indicating a success
|
7
|
+
class MainStatusError < Error
|
8
|
+
attr_reader :error_sub_status
|
9
|
+
|
10
|
+
def initialize(error_sub_status)
|
11
|
+
@error_sub_status = error_sub_status
|
12
|
+
super "Error sub status was: '#{error_sub_status}'"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class UnauthorizedError < MainStatusError; end
|
17
|
+
class UnauthenticatedError < MainStatusError; end
|
18
|
+
class UnexpectedError < MainStatusError; end
|
19
|
+
|
20
|
+
# A map of brreg's sub status codes to error class we will raise
|
21
|
+
RESPONSE_SUB_STATUS_CODE_TO_ERROR = {
|
22
|
+
-100 => UnauthorizedError,
|
23
|
+
-101 => UnauthenticatedError,
|
24
|
+
-1000 => UnexpectedError
|
25
|
+
}.freeze
|
26
|
+
|
27
|
+
def initialize(response)
|
28
|
+
@response = response
|
29
|
+
@header = response.header
|
30
|
+
end
|
31
|
+
|
32
|
+
# rubocop:disable Metrics/MethodLength
|
33
|
+
def raise_error_or_return_response!
|
34
|
+
return @response if @header.success?
|
35
|
+
|
36
|
+
error_class = nil
|
37
|
+
error_sub_status = nil
|
38
|
+
|
39
|
+
case @header.sub_statuses.length
|
40
|
+
when 0
|
41
|
+
error_sub_status = 'Not included in response'
|
42
|
+
error_class = UnexpectedError
|
43
|
+
when 1
|
44
|
+
error_sub_status = @header.sub_statuses[0]
|
45
|
+
code = error_sub_status[:code]
|
46
|
+
error_class = RESPONSE_SUB_STATUS_CODE_TO_ERROR.fetch(code) { UnexpectedError }
|
47
|
+
else
|
48
|
+
raise Error, "Expected 0 or 1 sub status. Got: #{@header.sub_statuses}"
|
49
|
+
end
|
50
|
+
|
51
|
+
raise error_class, error_sub_status
|
52
|
+
end
|
53
|
+
# rubocop:enable Metrics/MethodLength
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'builder'
|
4
|
+
|
5
|
+
module BrregGrunndata
|
6
|
+
class Client
|
7
|
+
# Translate a simple query string to an XML document to search with sok_enhet operation
|
8
|
+
#
|
9
|
+
# Given the query "STATOIL" the final XML to be put in soap search_request
|
10
|
+
# will be:
|
11
|
+
#
|
12
|
+
# <![CDATA[
|
13
|
+
# <?xml version="1.0"?>
|
14
|
+
# <BrAixXmlRequest RequestName="BrErfrSok">
|
15
|
+
# <BrErfrSok>
|
16
|
+
# <BrSokeStreng>STATOIL</BrSokeStreng>
|
17
|
+
# <MaxTreffReturneres>1000</MaxTreffReturneres>
|
18
|
+
# <ReturnerIngenHvisMax>true</ReturnerIngenHvisMax>
|
19
|
+
# <RequestingIPAddr>010.001.052.011</RequestingIPAddr>
|
20
|
+
# <RequestingTjeneste>SOAP</RequestingTjeneste>
|
21
|
+
# <MedUnderenheter>true</MedUnderenheter>
|
22
|
+
# </BrErfrSok>
|
23
|
+
# </BrAixXmlRequest>
|
24
|
+
# ]]
|
25
|
+
#
|
26
|
+
# Which is great: Now we got a XML inside XML
|
27
|
+
# payload instead of something simple ;-)
|
28
|
+
#
|
29
|
+
# Attributes
|
30
|
+
#
|
31
|
+
# query - Your search string / query goes here
|
32
|
+
# first - How many do you want to get in return? (the limit)
|
33
|
+
# include_no_if_max - Do you want zero results if your search yields more
|
34
|
+
# results than the first X you asked for? I don't know
|
35
|
+
# why you would want that.
|
36
|
+
# with_subdivision - Do you want to include organization form BEDR og AAFY
|
37
|
+
# when you search?
|
38
|
+
# ip - Your client's IP. Seems to work with everything, as
|
39
|
+
# long as you have xxx.xxx.xxx.xxx where x is [0-9].
|
40
|
+
class SokEnhetQueryToXml
|
41
|
+
def initialize(query,
|
42
|
+
first: 100,
|
43
|
+
include_no_if_max: false,
|
44
|
+
with_subdivision: true,
|
45
|
+
ip: '010.001.052.011')
|
46
|
+
@query = query
|
47
|
+
@first = first
|
48
|
+
@ip = ip
|
49
|
+
@include_no_if_max = include_no_if_max
|
50
|
+
@with_subdivision = with_subdivision
|
51
|
+
end
|
52
|
+
|
53
|
+
def cdata
|
54
|
+
"<![CDATA[#{xml}]]>"
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
# rubocop:disable Metrics/MethodLength
|
60
|
+
def xml
|
61
|
+
data = {
|
62
|
+
br_aix_xml_request: {
|
63
|
+
:@RequestName => 'BrErfrSok',
|
64
|
+
br_erfr_sok: {
|
65
|
+
br_soke_streng: @query,
|
66
|
+
max_treff_returneres: @first,
|
67
|
+
returner_ingen_hvis_max: true,
|
68
|
+
requesting_IP_addr: @ip,
|
69
|
+
requesting_tjeneste: 'SOAP',
|
70
|
+
med_underenheter: @with_subdivision
|
71
|
+
}
|
72
|
+
}
|
73
|
+
}
|
74
|
+
|
75
|
+
options = { key_converter: :camelcase }
|
76
|
+
|
77
|
+
Gyoku.xml data, options
|
78
|
+
end
|
79
|
+
# rubocop:enable Metrics/MethodLength
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BrregGrunndata
|
4
|
+
# Contains configuration for the web service client
|
5
|
+
class Configuration
|
6
|
+
# WSDL is located at this URL
|
7
|
+
WSDL_URL = 'https://ws.brreg.no/grunndata/ErFr?WSDL'
|
8
|
+
|
9
|
+
# We have a saved WSDL at this location on disk
|
10
|
+
WSDL_PATH = "#{__dir__}/wsdl/grunndata.xml"
|
11
|
+
|
12
|
+
attr_reader :userid, :password,
|
13
|
+
:open_timeout, :read_timeout,
|
14
|
+
:logger, :log_level,
|
15
|
+
:wsdl
|
16
|
+
|
17
|
+
# rubocop:disable Metrics/ParameterLists
|
18
|
+
def initialize(
|
19
|
+
userid:,
|
20
|
+
password:,
|
21
|
+
open_timeout: 15,
|
22
|
+
read_timeout: 15,
|
23
|
+
logger: nil,
|
24
|
+
log_level: :info,
|
25
|
+
wsdl: WSDL_PATH
|
26
|
+
)
|
27
|
+
@userid = userid
|
28
|
+
@password = password
|
29
|
+
@open_timeout = open_timeout
|
30
|
+
@read_timeout = read_timeout
|
31
|
+
@logger = logger
|
32
|
+
@log_level = log_level
|
33
|
+
@wsdl = wsdl
|
34
|
+
end
|
35
|
+
# rubocop:enable Metrics/ParameterLists
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'types/factories'
|
4
|
+
require_relative 'utils'
|
5
|
+
|
6
|
+
module BrregGrunndata
|
7
|
+
# The service returns ruby objects with data fetched from API
|
8
|
+
#
|
9
|
+
# This interface has a higher abstraction than working directly
|
10
|
+
# with the Client, where the object you get back has coerced values
|
11
|
+
# instead of working with a hash of strings.
|
12
|
+
#
|
13
|
+
# @see Client
|
14
|
+
class Service
|
15
|
+
attr_reader :client
|
16
|
+
|
17
|
+
def initialize(client:)
|
18
|
+
@client = client
|
19
|
+
end
|
20
|
+
|
21
|
+
# Runs given operations concurrently
|
22
|
+
#
|
23
|
+
# Arguments
|
24
|
+
# operations - An array of operations to run concurrently
|
25
|
+
# The named operations must be defined as
|
26
|
+
# methods on the service and they must return same type.
|
27
|
+
# args - All other arguments are passed on to each operations.
|
28
|
+
def run_concurrently(operations, **args)
|
29
|
+
results = Utils::ConcurrentOperations.new(self, operations, args).call
|
30
|
+
|
31
|
+
return nil if results.any?(&:nil?)
|
32
|
+
|
33
|
+
results.reduce { |acc, elem| acc.merge elem }
|
34
|
+
end
|
35
|
+
|
36
|
+
# Get basic mini data of an organization
|
37
|
+
#
|
38
|
+
# Arguments
|
39
|
+
# orgnr - The orgnr you are searching for
|
40
|
+
#
|
41
|
+
# @return BrregGrunndata::Types::Organization
|
42
|
+
def hent_basisdata_mini(orgnr:)
|
43
|
+
Types::FromResponseFactory.organization client.hent_basisdata_mini orgnr: orgnr
|
44
|
+
end
|
45
|
+
|
46
|
+
# Get contact information for an organization
|
47
|
+
#
|
48
|
+
# Arguments
|
49
|
+
# orgnr - The orgnr you are searching for
|
50
|
+
#
|
51
|
+
# @return BrregGrunndata::Types::Organization
|
52
|
+
def hent_kontaktdata(orgnr:)
|
53
|
+
Types::FromResponseFactory.organization client.hent_kontaktdata orgnr: orgnr
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
|
5
|
+
module BrregGrunndata
|
6
|
+
module Types
|
7
|
+
class Address < Base
|
8
|
+
attribute :street, Types::String
|
9
|
+
attribute :postal_code, Types::String
|
10
|
+
attribute :postal_area, Types::String
|
11
|
+
attribute :municipality_number, Types::String
|
12
|
+
attribute :municipality, Types::String
|
13
|
+
attribute :country_code, Types::String
|
14
|
+
attribute :country, Types::String
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry-struct'
|
4
|
+
require 'dry-types'
|
5
|
+
|
6
|
+
require_relative '../utils'
|
7
|
+
|
8
|
+
module BrregGrunndata
|
9
|
+
module Types
|
10
|
+
# Include Dry basic types
|
11
|
+
#
|
12
|
+
# Whenever you see something like Types::String it is a dry type
|
13
|
+
include Dry::Types.module
|
14
|
+
|
15
|
+
# Base class for all BrregGrunndata's types
|
16
|
+
class Base < Dry::Struct
|
17
|
+
# Allow missing keys in the input data.
|
18
|
+
# Missing data will get default value or nil.
|
19
|
+
constructor_type :schema
|
20
|
+
|
21
|
+
# Merges two base objects together and returns a new instance
|
22
|
+
#
|
23
|
+
# @return a new instance of self, filled/merged with data from other
|
24
|
+
def merge(other)
|
25
|
+
Utils::BaseTypeMerger.new(self, other).merge
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'organization'
|
4
|
+
require_relative 'organizational_form'
|
5
|
+
require_relative 'address'
|
6
|
+
|
7
|
+
module BrregGrunndata
|
8
|
+
module Types
|
9
|
+
module Factory
|
10
|
+
module_function
|
11
|
+
|
12
|
+
# Creates an organization from given hash
|
13
|
+
#
|
14
|
+
# rubocop:disable Metrics/MethodLength
|
15
|
+
#
|
16
|
+
# @return BrregGrunndata::Types::Organization
|
17
|
+
def organization(h)
|
18
|
+
Organization.new(
|
19
|
+
orgnr: h.fetch(:organisasjonsnummer),
|
20
|
+
organizational_form: organizational_form(h[:organisasjonsform]),
|
21
|
+
|
22
|
+
name: h.dig(:navn, :navn1),
|
23
|
+
|
24
|
+
telephone_number: h[:telefonnummer],
|
25
|
+
telefax_number: h[:telefaksnummer],
|
26
|
+
mobile_number: h[:mobiltelefonnummer],
|
27
|
+
email: h[:epostadresse],
|
28
|
+
web_page: h[:hjemmesideadresse],
|
29
|
+
|
30
|
+
business_address: business_address(h[:forretnings_adresse]),
|
31
|
+
postal_address: postal_address(h[:post_adresse])
|
32
|
+
)
|
33
|
+
end
|
34
|
+
# rubocop:enable Metrics/MethodLength
|
35
|
+
|
36
|
+
# Creates a address for given Hash
|
37
|
+
#
|
38
|
+
# @return BrregGrunndata::Types::Address
|
39
|
+
def business_address(hash)
|
40
|
+
__address hash
|
41
|
+
end
|
42
|
+
|
43
|
+
# Creates a address for given Hash
|
44
|
+
#
|
45
|
+
# @return BrregGrunndata::Types::Address
|
46
|
+
def postal_address(hash)
|
47
|
+
__address hash
|
48
|
+
end
|
49
|
+
|
50
|
+
# Creates a organizational form for given Hash
|
51
|
+
#
|
52
|
+
# @return BrregGrunndata::Types::OrganizationalForm
|
53
|
+
def organizational_form(h)
|
54
|
+
return nil if h.nil?
|
55
|
+
|
56
|
+
OrganizationalForm.new(
|
57
|
+
name: h[:orgform],
|
58
|
+
description: h[:orgform_beskrivelse]
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
# As of writing, keys for postal and business address
|
63
|
+
# are the same, so the are both initialized here
|
64
|
+
def __address(h)
|
65
|
+
return nil if h.nil?
|
66
|
+
|
67
|
+
Address.new(
|
68
|
+
street: h[:adresse1],
|
69
|
+
postal_code: h[:postnr],
|
70
|
+
postal_area: h[:poststed],
|
71
|
+
municipality_number: h[:kommunenummer],
|
72
|
+
municipality: h[:kommune],
|
73
|
+
country_code: h[:landkode],
|
74
|
+
country: h[:land]
|
75
|
+
)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Contains methods for extracting datafrom client responses
|
80
|
+
# and building defined types from it.
|
81
|
+
module FromResponseFactory
|
82
|
+
module_function
|
83
|
+
|
84
|
+
# Creates an organization from given response
|
85
|
+
#
|
86
|
+
# @return BrregGrunndata::Types::Organization
|
87
|
+
def organization(response)
|
88
|
+
Factory.organization response.message
|
89
|
+
rescue Client::Response::MessageEmptyError
|
90
|
+
nil
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|