brreg_grunndata 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|