uk-hs_code 0.1.3 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3da63080141113f6a0de5d28178905c9534f4a6994e0ef27b644e9ffc91acc9e
4
- data.tar.gz: 6c38e1ce4e3fe9a578b38346e3db3590d8ca86eafce9cc06d6533c1835a611a5
3
+ metadata.gz: a4dd60a00dee0539c09bcef47261cf9433b3dcc00f81cb9a71eb8f22899cf096
4
+ data.tar.gz: 857040a2ba439ceda069276cd50606a371650e4198a1d5a8d660cd2b1de16737
5
5
  SHA512:
6
- metadata.gz: bac45bb5c7d8f54c061da3c6278b75f2543e1d99865672860c14963ec2250ed49bb1ad27772c14405c4e7b8dab6c7a0fa2233ccd2f6f2297924d3379d6e65d85
7
- data.tar.gz: 1135dcf855bf24a2a6df4d2c56e40ae54c783bbe97b681182e9db9e89d176035af73c3e2758ef8871614409f5ba511a6fd3edf096d6828564051272ee9c1c4f9
6
+ metadata.gz: fba1933687255a01899e19f066b5372d0e1b764713372fa91e5bb9fe572cecc8b8afebc2c87681af00d50a100b1cc4de3eec5034f7a4b5f0b8d4d217e3bb9883
7
+ data.tar.gz: 88b271cee51462b6471a45a2899982508a5e243814ef658bf78abddfb341041fe18f2e509f9a666dd0d81c39c9c36773fe7c75f7913ef97e7dec6e5b2a368729
data/.rubocop.yml CHANGED
@@ -1,5 +1,7 @@
1
1
  AllCops:
2
- TargetRubyVersion: 2.6
2
+ TargetRubyVersion: 3.0
3
+ NewCops: enable
4
+ SuggestExtensions: false
3
5
 
4
6
  Style/StringLiterals:
5
7
  Enabled: true
@@ -12,9 +14,6 @@ Style/StringLiteralsInInterpolation:
12
14
  Style/Documentation:
13
15
  Enabled: false
14
16
 
15
- Layout/LineLength:
16
- Max: 120
17
-
18
17
  Layout/LineLength:
19
18
  Enabled: false
20
19
 
@@ -23,3 +22,21 @@ Metrics/AbcSize:
23
22
 
24
23
  Metrics/BlockLength:
25
24
  Enabled: false
25
+
26
+ Metrics/MethodLength:
27
+ Enabled: false
28
+
29
+ Metrics/CyclomaticComplexity:
30
+ Enabled: false
31
+
32
+ Metrics/PerceivedComplexity:
33
+ Enabled: false
34
+
35
+ Metrics/ParameterLists:
36
+ Enabled: false
37
+
38
+ Gemspec/DevelopmentDependencies:
39
+ Enabled: false
40
+
41
+ Gemspec/RequireMFA:
42
+ Enabled: false
data/Gemfile.lock CHANGED
@@ -1,14 +1,22 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- uk-hs_code (0.1.3)
4
+ uk-hs_code (0.3.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
+ addressable (2.9.0)
10
+ public_suffix (>= 2.0.2, < 8.0)
9
11
  ast (2.4.2)
12
+ base64 (0.3.0)
13
+ bigdecimal (4.1.3)
10
14
  coderay (1.1.3)
15
+ crack (1.0.1)
16
+ bigdecimal
17
+ rexml
11
18
  diff-lcs (1.5.0)
19
+ hashdiff (1.2.1)
12
20
  json (2.6.3)
13
21
  method_source (1.0.0)
14
22
  parallel (1.22.1)
@@ -17,6 +25,8 @@ GEM
17
25
  pry (0.14.2)
18
26
  coderay (~> 1.1)
19
27
  method_source (~> 1.0)
28
+ public_suffix (7.0.5)
29
+ racc (1.8.1)
20
30
  rainbow (3.1.1)
21
31
  rake (13.0.6)
22
32
  regexp_parser (2.7.0)
@@ -48,17 +58,24 @@ GEM
48
58
  parser (>= 3.1.1.0)
49
59
  ruby-progressbar (1.11.0)
50
60
  unicode-display_width (2.4.2)
61
+ webmock (3.34.0)
62
+ addressable (>= 2.8.0)
63
+ crack (>= 0.3.2)
64
+ hashdiff (>= 0.4.0, < 2.0.0)
51
65
 
52
66
  PLATFORMS
53
67
  arm64-darwin-23
54
68
  x86_64-linux
55
69
 
56
70
  DEPENDENCIES
71
+ base64
57
72
  pry
73
+ racc (~> 1.8)
58
74
  rake (~> 13.0)
59
75
  rspec (~> 3.0)
60
76
  rubocop
61
77
  uk-hs_code!
78
+ webmock (~> 3.0)
62
79
 
63
80
  BUNDLED WITH
64
81
  2.4.5
data/README.md CHANGED
@@ -1,52 +1,92 @@
1
1
  # Uk::HsCode
2
2
 
3
- uk-hs_code enables to dynamically search Harmonized Codes via UK Trade Tariff API, https://api.trade-tariff.service.gov.uk/#gov-uk-trade-tariff-api.
3
+ `uk-hs_code` enables communication with the UK Trade Tariff Public API v2 (https://www.trade-tariff.service.gov.uk/api/v2).
4
4
 
5
5
  ## Installation
6
6
 
7
- The repository is here: https://github.com/enigmatt-pl/uk-hs_code.git
8
-
9
7
  Add to your Gemfile:
10
8
 
11
- $ gem 'uk-hs_code'
9
+ ```ruby
10
+ gem 'uk-hs_code'
11
+ ```
12
+
13
+ And run:
14
+
15
+ ```bash
16
+ bundle install
17
+ ```
12
18
 
13
- And:
19
+ ## Configuration
14
20
 
15
- $ bundle install
21
+ Configure the gem during application startup (e.g., `config/initializers/uk_hs_code.rb`):
16
22
 
23
+ ```ruby
24
+ Uk::HsCode.configure do |config|
25
+ # Target UK tariff (:uk, default) or Northern Ireland tariff (:xi)
26
+ config.service = :uk
27
+
28
+ # Optional: Network timeouts in seconds (default: 10)
29
+ config.timeout = 15
30
+ end
31
+ ```
17
32
 
18
33
  ## Usage
19
34
 
35
+ ### Searching by Commodity Description
36
+
20
37
  ```ruby
21
- # Get all HS codes matching 'table' reference
38
+ # Free-text commodity search
39
+ result = Uk::HsCode.search('cotton t-shirt')
22
40
 
23
- > refs = Uk::HsCode.search('table')
41
+ # Result acts as an Enumerable Array of records
42
+ result.first.code
43
+ # => "6109100010"
24
44
 
25
- > refs.inspect
45
+ result.first.title
46
+ # => "T-shirts"
26
47
 
27
- # => [#<Uk::HsCode::Record id="6133", type="search_reference", attributes=#<Uk::HsCode::Record title="table, billiard and bagatelle", referenced_id="9504", referenced_class="Heading", goods_nomenclature_item_id="9504000000", productline_suffix="80", goods_nomenclature_sid=54540>>,
28
- #<Uk::HsCode::Record id="11204", type="search_reference", attributes=#<Uk::HsCode::Record title="table cloths, knitted or crocheted", referenced_id="6302", referenced_class="Heading", goods_nomenclature_item_id="6302000000", productline_suffix="80", goods_nomenclature_sid=43645>>,
29
- #<Uk::HsCode::Record id="11205", type="search_reference", attributes=#<Uk::HsCode::Record title="tablecloths, knitted or crocheted", referenced_id="6302", referenced_class="Heading", goods_nomenclature_item_id="6302000000", productline_suffix="80", goods_nomenclature_sid=43645>>,
30
- #<Uk::HsCode::Record id="7289", type="search_reference", attributes=#<Uk::HsCode::Record title="tablecloths, of paper", referenced_id="4818300000-80", referenced_class="Commodity", goods_nomenclature_item_id="4818300000", productline_suffix="80", goods_nomenclature_sid=40686>>,
31
- #<Uk::HsCode::Record id="11206", type="search_reference", attributes=#<Uk::HsCode::Record title="table cloths, other than knitted or crocheted", referenced_id="6302510000-10", referenced_class="Subheading", goods_nomenclature_item_id="6302510000", productline_suffix="10", goods_nomenclature_sid=43679>>
48
+ result.first.declarable
49
+ # => true
32
50
 
33
- # Get only HS codes with their titles matching 'carpet' reference
51
+ # Structured access to categorized match groups
52
+ result.commodities.each do |commodity|
53
+ puts "#{commodity.code}: #{commodity.description} (declarable: #{commodity.declarable})"
54
+ end
34
55
 
35
- > refs = Uk::HsCode.search_hs_codes('carpet')
56
+ result.references.each do |reference|
57
+ puts "#{reference.code}: #{reference.title}"
58
+ end
59
+ ```
60
+
61
+ ### Searching by Exact Commodity Code
62
+
63
+ ```ruby
64
+ result = Uk::HsCode.search('6109100010')
65
+
66
+ result.first.code
67
+ # => "6109100010"
36
68
 
37
- > refs.inspect
69
+ result.first.type
70
+ # => "exact"
71
+ ```
72
+
73
+ ### Search References (Alphabetical Index Terms)
74
+
75
+ ```ruby
76
+ records = Uk::HsCode.search_references('table')
38
77
 
39
- # => [#<Uk::HsCode::Record code="8451808000", title="carpet cleaning machines liquid injection">,
40
- #<Uk::HsCode::Record code="4008000000", title="carpet underlay, rubber">,
41
- #<Uk::HsCode::Record code="5402000000", title="carpet yarn, of man-made filaments">]
78
+ records.first.code
79
+ # => "9504000000"
42
80
 
81
+ records.first.title
82
+ # => "table, billiard and bagatelle"
43
83
  ```
44
84
 
45
85
  ## Development
46
86
 
47
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. To test with static code analyzer, run `rake rubocop`. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
87
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake` to run tests and RuboCop. You can also run `bin/console` for an interactive prompt.
48
88
 
49
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
89
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`.
50
90
 
51
91
  ## Contributing
52
92
 
@@ -3,7 +3,6 @@
3
3
  require "net/http"
4
4
  require "uri"
5
5
  require "json"
6
- # require "record"
7
6
 
8
7
  module Uk
9
8
  module HsCode
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+
7
+ module Uk
8
+ module HsCode
9
+ class Client
10
+ def initialize(configuration: Uk::HsCode.configuration, base_url: nil)
11
+ @configuration = configuration
12
+ @base_url = base_url || @configuration.base_url
13
+ end
14
+
15
+ def get(path, params = {})
16
+ uri = build_uri(path, params)
17
+ request = Net::HTTP::Get.new(uri)
18
+ apply_headers(request)
19
+
20
+ http = Net::HTTP.new(uri.host, uri.port)
21
+ http.use_ssl = true if uri.scheme == "https"
22
+ http.open_timeout = @configuration.timeout
23
+ http.read_timeout = @configuration.timeout
24
+
25
+ response = http.request(request)
26
+ handle_response(response)
27
+ end
28
+
29
+ private
30
+
31
+ def apply_headers(request)
32
+ request["User-Agent"] = @configuration.user_agent
33
+ request["Accept"] = "application/json"
34
+ end
35
+
36
+ def build_uri(path, params)
37
+ full_url = "#{@base_url}#{path}"
38
+ uri = URI.parse(full_url)
39
+ if params && !params.empty?
40
+ query_str = URI.encode_www_form(params)
41
+ uri.query = uri.query ? "#{uri.query}&#{query_str}" : query_str
42
+ end
43
+ uri
44
+ end
45
+
46
+ def handle_response(response)
47
+ raise Uk::HsCode::Error, "Request failed with status: #{response.code} - #{response.message}" unless response.is_a?(Net::HTTPSuccess)
48
+
49
+ JSON.parse(response.body)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uk
4
+ module HsCode
5
+ class Configuration
6
+ SERVICE_URLS = {
7
+ uk: "https://www.trade-tariff.service.gov.uk/api/v2",
8
+ xi: "https://www.trade-tariff.service.gov.uk/xi/api/v2"
9
+ }.freeze
10
+
11
+ DEFAULT_TIMEOUT = 10
12
+
13
+ attr_accessor :timeout, :user_agent
14
+ attr_writer :base_url
15
+ attr_reader :service
16
+
17
+ def initialize
18
+ @service = :uk
19
+ @base_url = nil
20
+ @timeout = DEFAULT_TIMEOUT
21
+ @user_agent = "UK HS Code gem/#{Uk::HsCode::VERSION}"
22
+ end
23
+
24
+ def service=(val)
25
+ symbolized = val.to_s.downcase.to_sym
26
+ raise ArgumentError, "Unsupported service: #{val}. Use :uk or :xi." unless SERVICE_URLS.key?(symbolized)
27
+
28
+ @service = symbolized
29
+ end
30
+
31
+ def base_url
32
+ @base_url || SERVICE_URLS.fetch(@service)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uk
4
+ module HsCode
5
+ module Entities
6
+ class Commodity
7
+ attr_reader :code, :description, :score, :declarable, :producline_suffix,
8
+ :number_indents, :chapter, :heading, :section, :ancestor_descriptions
9
+
10
+ def initialize(code:, description:, score: nil, declarable: true, producline_suffix: "80",
11
+ number_indents: 0, chapter: nil, heading: nil, section: nil, ancestor_descriptions: [])
12
+ @code = code
13
+ @description = description
14
+ @score = score
15
+ @declarable = declarable
16
+ @producline_suffix = producline_suffix
17
+ @number_indents = number_indents
18
+ @chapter = chapter
19
+ @heading = heading
20
+ @section = section
21
+ @ancestor_descriptions = ancestor_descriptions || []
22
+ end
23
+
24
+ def title
25
+ @description
26
+ end
27
+
28
+ def goods_nomenclature_item_id
29
+ @code
30
+ end
31
+
32
+ def type
33
+ "commodity"
34
+ end
35
+
36
+ def to_h
37
+ {
38
+ code: @code,
39
+ description: @description,
40
+ title: title,
41
+ score: @score,
42
+ declarable: @declarable,
43
+ producline_suffix: @producline_suffix,
44
+ number_indents: @number_indents,
45
+ chapter: @chapter,
46
+ heading: @heading,
47
+ section: @section,
48
+ ancestor_descriptions: @ancestor_descriptions,
49
+ type: type
50
+ }
51
+ end
52
+
53
+ def ==(other)
54
+ other.is_a?(Commodity) && code == other.code && description == other.description
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uk
4
+ module HsCode
5
+ module Entities
6
+ class ExactMatch
7
+ attr_reader :code, :endpoint, :description
8
+
9
+ def initialize(code:, endpoint: "commodities", description: nil)
10
+ @code = code
11
+ @endpoint = endpoint
12
+ @description = description || "Exact match for #{code}"
13
+ end
14
+
15
+ def title
16
+ @description
17
+ end
18
+
19
+ def goods_nomenclature_item_id
20
+ @code
21
+ end
22
+
23
+ def declarable
24
+ true
25
+ end
26
+
27
+ def score
28
+ nil
29
+ end
30
+
31
+ def type
32
+ "exact"
33
+ end
34
+
35
+ def to_h
36
+ {
37
+ code: @code,
38
+ endpoint: @endpoint,
39
+ title: title,
40
+ description: @description,
41
+ type: type
42
+ }
43
+ end
44
+
45
+ def ==(other)
46
+ other.is_a?(ExactMatch) && code == other.code && endpoint == other.endpoint
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uk
4
+ module HsCode
5
+ module Entities
6
+ class SearchReference
7
+ attr_reader :title, :code, :reference_class, :description, :score, :declarable
8
+
9
+ def initialize(title:, code:, reference_class: "Commodity", description: nil, score: nil, declarable: true)
10
+ @title = title
11
+ @code = code
12
+ @reference_class = reference_class
13
+ @description = description || title
14
+ @score = score
15
+ @declarable = declarable
16
+ end
17
+
18
+ def goods_nomenclature_item_id
19
+ @code
20
+ end
21
+
22
+ def type
23
+ "reference"
24
+ end
25
+
26
+ def to_h
27
+ {
28
+ title: @title,
29
+ code: @code,
30
+ reference_class: @reference_class,
31
+ description: @description,
32
+ score: @score,
33
+ declarable: @declarable,
34
+ type: type
35
+ }
36
+ end
37
+
38
+ def ==(other)
39
+ other.is_a?(SearchReference) && code == other.code && title == other.title
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uk
4
+ module HsCode
5
+ module Entities
6
+ class SearchResult < Array
7
+ attr_reader :query, :type, :commodities, :references, :exact_match
8
+
9
+ def initialize(records = [], query: nil, type: "null_match", commodities: [], references: [], exact_match: nil)
10
+ @query = query
11
+ @type = type
12
+ @commodities = commodities || []
13
+ @references = references || []
14
+ @exact_match = exact_match
15
+ super(records)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uk
4
+ module HsCode
5
+ module Parsers
6
+ class SearchResponseParser
7
+ def self.call(response_hash, query: nil)
8
+ new(response_hash, query: query).call
9
+ end
10
+
11
+ def initialize(response_hash, query: nil)
12
+ @response_hash = response_hash || {}
13
+ @query = query
14
+ end
15
+
16
+ def call
17
+ data = @response_hash["data"] || @response_hash[:data]
18
+ return Entities::SearchResult.new([], query: @query) unless data
19
+
20
+ type = data["type"] || data[:type]
21
+ attributes = data["attributes"] || data[:attributes] || {}
22
+
23
+ case type
24
+ when "exact_search"
25
+ parse_exact_search(attributes)
26
+ when "fuzzy_search"
27
+ parse_fuzzy_search(attributes)
28
+ else
29
+ Entities::SearchResult.new([], query: @query, type: type.to_s)
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def parse_exact_search(attributes)
36
+ entry = attributes["entry"] || attributes[:entry] || {}
37
+ code = entry["id"] || entry[:id]
38
+ endpoint = entry["endpoint"] || entry[:endpoint] || "commodities"
39
+ return Entities::SearchResult.new([], query: @query, type: "exact_match") unless code
40
+
41
+ exact = Entities::ExactMatch.new(code: code, endpoint: endpoint)
42
+ Entities::SearchResult.new([exact], query: @query, type: "exact_match", exact_match: exact)
43
+ end
44
+
45
+ def parse_fuzzy_search(attributes)
46
+ commodities = extract_commodities(attributes)
47
+ references = extract_references(attributes)
48
+ records = (commodities + references).uniq(&:code)
49
+
50
+ Entities::SearchResult.new(
51
+ records,
52
+ query: @query,
53
+ type: "fuzzy_match",
54
+ commodities: commodities,
55
+ references: references
56
+ )
57
+ end
58
+
59
+ def extract_commodities(attributes)
60
+ commodities = []
61
+ gn_commodities = attributes.dig("goods_nomenclature_match", "commodities") || []
62
+
63
+ gn_commodities.each do |hit|
64
+ source = hit["_source"] || {}
65
+ code = source["goods_nomenclature_item_id"]
66
+ next if code.nil?
67
+
68
+ commodities << Entities::Commodity.new(
69
+ code: code,
70
+ description: source["description"],
71
+ score: hit["_score"],
72
+ declarable: source["declarable"],
73
+ producline_suffix: source["producline_suffix"],
74
+ number_indents: source["number_indents"],
75
+ chapter: source["chapter"],
76
+ heading: source["heading"],
77
+ section: source["section"],
78
+ ancestor_descriptions: source["ancestor_descriptions"]
79
+ )
80
+ end
81
+
82
+ commodities
83
+ end
84
+
85
+ def extract_references(attributes)
86
+ references = []
87
+ ref_commodities = attributes.dig("reference_match", "commodities") || []
88
+
89
+ ref_commodities.each do |hit|
90
+ source = hit["_source"] || {}
91
+ ref_data = source["reference"] || {}
92
+ code = ref_data["goods_nomenclature_item_id"]
93
+ next if code.nil?
94
+
95
+ references << Entities::SearchReference.new(
96
+ code: code,
97
+ title: source["title"],
98
+ description: ref_data["description"] || source["title"],
99
+ score: hit["_score"],
100
+ declarable: ref_data["declarable"],
101
+ reference_class: source["reference_class"] || "Commodity"
102
+ )
103
+ end
104
+
105
+ references
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -2,6 +2,79 @@
2
2
 
3
3
  module Uk
4
4
  module HsCode
5
- class Record < OpenStruct; end
5
+ class Record
6
+ attr_reader :score, :declarable, :type
7
+
8
+ def initialize(code: nil, title: nil, description: nil, score: nil, declarable: nil, type: nil, **extra)
9
+ @extra = extra || {}
10
+ @code = code || @extra[:goods_nomenclature_item_id] || @extra["goods_nomenclature_item_id"]
11
+ @title = title || description || @extra[:negated_title] || @extra["negated_title"]
12
+ @description = description || title
13
+ @score = score
14
+ @declarable = declarable
15
+ @type = type
16
+ end
17
+
18
+ def code
19
+ @code || self["code"] || self["goods_nomenclature_item_id"]
20
+ end
21
+
22
+ def title
23
+ @title || self["title"] || self["description"]
24
+ end
25
+
26
+ def description
27
+ @description || self["description"] || self["title"]
28
+ end
29
+
30
+ def goods_nomenclature_item_id
31
+ code
32
+ end
33
+
34
+ def attributes
35
+ self["attributes"] || self
36
+ end
37
+
38
+ def [](key)
39
+ instance_variable_get("@#{key}") || @extra[key.to_s] || @extra[key.to_sym]
40
+ end
41
+
42
+ def []=(key, val)
43
+ instance_variable_set("@#{key}", val)
44
+ @extra[key.to_s] = val
45
+ @extra[key.to_sym] = val
46
+ end
47
+
48
+ def method_missing(method_name, *args, &block)
49
+ str_key = method_name.to_s
50
+ if str_key.end_with?("=")
51
+ self[str_key.chop] = args.first
52
+ elsif @extra.key?(str_key) || @extra.key?(method_name)
53
+ self[method_name]
54
+ else
55
+ super
56
+ end
57
+ end
58
+
59
+ def respond_to_missing?(method_name, include_private = false)
60
+ str_key = method_name.to_s.chomp("=")
61
+ @extra.key?(str_key) || @extra.key?(str_key.to_sym) || super
62
+ end
63
+
64
+ def to_h
65
+ {
66
+ code: code,
67
+ title: title,
68
+ description: description,
69
+ score: score,
70
+ declarable: declarable,
71
+ type: type
72
+ }
73
+ end
74
+
75
+ def ==(other)
76
+ other.is_a?(Record) && code == other.code && title == other.title
77
+ end
78
+ end
6
79
  end
7
80
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uk
4
+ module HsCode
5
+ module Services
6
+ class SearchReferencesService
7
+ def self.call(...)
8
+ new(...).call
9
+ end
10
+
11
+ def initialize(params = {}, client: Client.new, **keyword_params)
12
+ merged_params = keyword_params.empty? ? params : keyword_params
13
+ @params = merged_params.is_a?(Hash) ? merged_params : { query: merged_params }
14
+ @client = client
15
+ end
16
+
17
+ def call
18
+ validate_prerequisites!
19
+ execute_workflow
20
+ end
21
+
22
+ private
23
+
24
+ def validate_prerequisites!
25
+ raise ArgumentError, "Missing required query" if query.empty?
26
+ end
27
+
28
+ def execute_workflow
29
+ raw_response = @client.get("/search_references.json", { "query[letter]" => query })
30
+ data = raw_response["data"] || []
31
+ data.map do |item|
32
+ attrs = item["attributes"] || {}
33
+ Record.new(
34
+ code: attrs["goods_nomenclature_item_id"],
35
+ title: attrs["title"],
36
+ id: item["id"],
37
+ goods_nomenclature_sid: attrs["goods_nomenclature_sid"],
38
+ productline_suffix: attrs["productline_suffix"]
39
+ )
40
+ end
41
+ end
42
+
43
+ def query
44
+ (@params[:query] || @params["query"] || @params[:letter] || @params["letter"]).to_s.strip
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uk
4
+ module HsCode
5
+ module Services
6
+ class SearchService
7
+ def self.call(...)
8
+ new(...).call
9
+ end
10
+
11
+ def initialize(params = {}, client: Client.new, **keyword_params)
12
+ merged_params = keyword_params.empty? ? params : keyword_params
13
+ @params = merged_params.is_a?(Hash) ? merged_params : { query: merged_params }
14
+ @client = client
15
+ end
16
+
17
+ def call
18
+ validate_prerequisites!
19
+ execute_workflow
20
+ end
21
+
22
+ private
23
+
24
+ def validate_prerequisites!
25
+ raise ArgumentError, "Missing required query" if query.empty?
26
+ end
27
+
28
+ def execute_workflow
29
+ raw_response = @client.get("/search", { q: query })
30
+ Parsers::SearchResponseParser.call(raw_response, query: query)
31
+ end
32
+
33
+ def query
34
+ (@params[:query] || @params["query"] || @params[:q] || @params["q"]).to_s.strip
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Uk
4
4
  module HsCode
5
- VERSION = "0.1.3"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
data/lib/uk/hs_code.rb CHANGED
@@ -1,29 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "hs_code/version"
4
- require_relative "hs_code/api_request"
4
+ require_relative "hs_code/configuration"
5
+ require_relative "hs_code/entities/commodity"
6
+ require_relative "hs_code/entities/search_reference"
7
+ require_relative "hs_code/entities/exact_match"
8
+ require_relative "hs_code/entities/search_result"
5
9
  require_relative "hs_code/record"
10
+ require_relative "hs_code/client"
11
+ require_relative "hs_code/parsers/search_response_parser"
12
+ require_relative "hs_code/services/search_service"
13
+ require_relative "hs_code/services/search_references_service"
14
+ require_relative "hs_code/api_request"
6
15
 
7
16
  module Uk
8
17
  module HsCode
9
- API_ENDPOINT = "https://www.trade-tariff.service.gov.uk/api"
18
+ API_ENDPOINT = "https://www.trade-tariff.service.gov.uk/api/v2"
10
19
 
11
20
  class Error < StandardError; end
12
21
 
13
22
  class << self
23
+ def configuration
24
+ @configuration ||= Configuration.new
25
+ end
26
+
27
+ def configure
28
+ yield(configuration) if block_given?
29
+ end
30
+
31
+ def reset_configuration!
32
+ @configuration = Configuration.new
33
+ end
34
+
14
35
  def search(query)
15
- search_references(query)
36
+ Services::SearchService.call(query: query)
16
37
  end
17
38
 
18
39
  def search_hs_codes(query)
19
- result = search_references(query)
20
- result.map { |a| Record.new(code: a.attributes.goods_nomenclature_item_id, title: a.attributes.title) }
40
+ search(query)
21
41
  end
22
42
 
23
- private
24
-
25
43
  def search_references(query)
26
- ApiRequest.new("#{API_ENDPOINT}/v2/search_references.json", { query: { letter: query } }).get
44
+ Services::SearchReferencesService.call(query: query)
27
45
  end
28
46
  end
29
47
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/uk/hs_code/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "uk-hs_code"
7
+ spec.version = Uk::HsCode::VERSION
8
+ spec.authors = ["Mateusz Michalski"]
9
+ spec.email = ["enigmatt.priv@gmail.com"]
10
+
11
+ spec.summary = "Enables communication with Trade Tariff Public API V2."
12
+ spec.description = "Trade Tariff Public API V2 communicator."
13
+ spec.homepage = "https://github.com/enigmatt-pl/uk-hs_code.git"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.0.0"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = spec.homepage
19
+ spec.metadata["changelog_uri"] = spec.homepage
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(__dir__) do
24
+ `git ls-files -z`.split("\x0").reject do |f|
25
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|circleci)|appveyor)})
26
+ end
27
+ end
28
+ spec.bindir = "exe"
29
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ["lib"]
31
+
32
+ spec.add_development_dependency "base64"
33
+ spec.add_development_dependency "pry"
34
+ spec.add_development_dependency "racc", "~> 1.8"
35
+ spec.add_development_dependency "rake", "~> 13.0"
36
+ spec.add_development_dependency "rspec", "~> 3.0"
37
+ spec.add_development_dependency "rubocop"
38
+ spec.add_development_dependency "webmock", "~> 3.0"
39
+ end
metadata CHANGED
@@ -1,17 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: uk-hs_code
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mateusz Michalski
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2025-05-23 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
- name: pry
13
+ name: base64
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
17
16
  - - ">="
@@ -25,7 +24,7 @@ dependencies:
25
24
  - !ruby/object:Gem::Version
26
25
  version: '0'
27
26
  - !ruby/object:Gem::Dependency
28
- name: rubocop
27
+ name: pry
29
28
  requirement: !ruby/object:Gem::Requirement
30
29
  requirements:
31
30
  - - ">="
@@ -38,6 +37,20 @@ dependencies:
38
37
  - - ">="
39
38
  - !ruby/object:Gem::Version
40
39
  version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: racc
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.8'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.8'
41
54
  - !ruby/object:Gem::Dependency
42
55
  name: rake
43
56
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +79,34 @@ dependencies:
66
79
  - - "~>"
67
80
  - !ruby/object:Gem::Version
68
81
  version: '3.0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rubocop
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
+ - !ruby/object:Gem::Dependency
97
+ name: webmock
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '3.0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '3.0'
69
110
  description: Trade Tariff Public API V2 communicator.
70
111
  email:
71
112
  - enigmatt.priv@gmail.com
@@ -83,9 +124,19 @@ files:
83
124
  - Rakefile
84
125
  - lib/uk/hs_code.rb
85
126
  - lib/uk/hs_code/api_request.rb
127
+ - lib/uk/hs_code/client.rb
128
+ - lib/uk/hs_code/configuration.rb
129
+ - lib/uk/hs_code/entities/commodity.rb
130
+ - lib/uk/hs_code/entities/exact_match.rb
131
+ - lib/uk/hs_code/entities/search_reference.rb
132
+ - lib/uk/hs_code/entities/search_result.rb
133
+ - lib/uk/hs_code/parsers/search_response_parser.rb
86
134
  - lib/uk/hs_code/record.rb
135
+ - lib/uk/hs_code/services/search_references_service.rb
136
+ - lib/uk/hs_code/services/search_service.rb
87
137
  - lib/uk/hs_code/version.rb
88
138
  - sig/uk/hs_code.rbs
139
+ - uk-hs_code.gemspec
89
140
  homepage: https://github.com/enigmatt-pl/uk-hs_code.git
90
141
  licenses:
91
142
  - MIT
@@ -93,7 +144,6 @@ metadata:
93
144
  homepage_uri: https://github.com/enigmatt-pl/uk-hs_code.git
94
145
  source_code_uri: https://github.com/enigmatt-pl/uk-hs_code.git
95
146
  changelog_uri: https://github.com/enigmatt-pl/uk-hs_code.git
96
- post_install_message:
97
147
  rdoc_options: []
98
148
  require_paths:
99
149
  - lib
@@ -101,15 +151,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
101
151
  requirements:
102
152
  - - ">="
103
153
  - !ruby/object:Gem::Version
104
- version: 2.6.0
154
+ version: 3.0.0
105
155
  required_rubygems_version: !ruby/object:Gem::Requirement
106
156
  requirements:
107
157
  - - ">="
108
158
  - !ruby/object:Gem::Version
109
159
  version: '0'
110
160
  requirements: []
111
- rubygems_version: 3.0.3.1
112
- signing_key:
161
+ rubygems_version: 3.6.7
113
162
  specification_version: 4
114
163
  summary: Enables communication with Trade Tariff Public API V2.
115
164
  test_files: []