brreg_grunndata 0.1.0

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