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.
@@ -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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrregGrunndata
4
+ class Error < StandardError
5
+ end
6
+ 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