uk-hs_code 0.2.0 → 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: 85e09b189b4a286a2766142d0b38f27be4bca6b5441ed8d6b3c274d1e3109036
4
- data.tar.gz: 1e76d26889a40ebdd5ebd1b70f3634cce6fb92a6518e7d046d109020e84053ab
3
+ metadata.gz: a4dd60a00dee0539c09bcef47261cf9433b3dcc00f81cb9a71eb8f22899cf096
4
+ data.tar.gz: 857040a2ba439ceda069276cd50606a371650e4198a1d5a8d660cd2b1de16737
5
5
  SHA512:
6
- metadata.gz: ea095185027cfd389a2cbe225db95313de464046f60a97d53d0df943a551667c873afd33d7059804041721cb5832639c2e7c974b82eefd6bf0ffe5e277556d86
7
- data.tar.gz: a65cf16cfba32f7dcfb611acf1af51b0e721cd7e24326913a8279ed38c6971f708be76a09c1ebe622ec7c40fc64bb34d58520abe7f83504e8e50008e40e62427
6
+ metadata.gz: fba1933687255a01899e19f066b5372d0e1b764713372fa91e5bb9fe572cecc8b8afebc2c87681af00d50a100b1cc4de3eec5034f7a4b5f0b8d4d217e3bb9883
7
+ data.tar.gz: 88b271cee51462b6471a45a2899982508a5e243814ef658bf78abddfb341041fe18f2e509f9a666dd0d81c39c9c36773fe7c75f7913ef97e7dec6e5b2a368729
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- uk-hs_code (0.2.0)
4
+ uk-hs_code (0.3.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -1,50 +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:
12
14
 
13
- And:
15
+ ```bash
16
+ bundle install
17
+ ```
14
18
 
15
- $ bundle install
19
+ ## Configuration
16
20
 
21
+ Configure the gem during application startup (e.g., `config/initializers/uk_hs_code.rb`):
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
- # Search HS codes matching free-text commodity description
22
- commodities = Uk::HsCode.search('cotton t-shirt')
38
+ # Free-text commodity search
39
+ result = Uk::HsCode.search('cotton t-shirt')
23
40
 
24
- commodities.first.code
41
+ # Result acts as an Enumerable Array of records
42
+ result.first.code
25
43
  # => "6109100010"
26
44
 
27
- commodities.first.title
45
+ result.first.title
28
46
  # => "T-shirts"
29
47
 
30
- commodities.first.declarable
48
+ result.first.declarable
31
49
  # => true
32
50
 
33
- # Get only HS codes with their titles
34
- records = Uk::HsCode.search_hs_codes('carpet')
51
+ # Structured access to categorized match groups
52
+ result.commodities.each do |commodity|
53
+ puts "#{commodity.code}: #{commodity.description} (declarable: #{commodity.declarable})"
54
+ end
55
+
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"
68
+
69
+ result.first.type
70
+ # => "exact"
71
+ ```
72
+
73
+ ### Search References (Alphabetical Index Terms)
74
+
75
+ ```ruby
76
+ records = Uk::HsCode.search_references('table')
35
77
 
36
78
  records.first.code
37
- # => "8451808000"
79
+ # => "9504000000"
38
80
 
39
81
  records.first.title
40
- # => "carpet cleaning machines liquid injection"
82
+ # => "table, billiard and bagatelle"
41
83
  ```
42
84
 
43
85
  ## Development
44
86
 
45
- 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.
46
88
 
47
- 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`.
48
90
 
49
91
  ## Contributing
50
92
 
@@ -7,20 +7,20 @@ require "json"
7
7
  module Uk
8
8
  module HsCode
9
9
  class Client
10
- BASE_URL = "https://www.trade-tariff.service.gov.uk/api/v2"
11
-
12
- def initialize(base_url: BASE_URL)
13
- @base_url = base_url
10
+ def initialize(configuration: Uk::HsCode.configuration, base_url: nil)
11
+ @configuration = configuration
12
+ @base_url = base_url || @configuration.base_url
14
13
  end
15
14
 
16
15
  def get(path, params = {})
17
16
  uri = build_uri(path, params)
18
17
  request = Net::HTTP::Get.new(uri)
19
- request["User-Agent"] = "UK HS Code gem/#{Uk::HsCode::VERSION}"
20
- request["Accept"] = "application/json"
18
+ apply_headers(request)
21
19
 
22
20
  http = Net::HTTP.new(uri.host, uri.port)
23
21
  http.use_ssl = true if uri.scheme == "https"
22
+ http.open_timeout = @configuration.timeout
23
+ http.read_timeout = @configuration.timeout
24
24
 
25
25
  response = http.request(request)
26
26
  handle_response(response)
@@ -28,6 +28,11 @@ module Uk
28
28
 
29
29
  private
30
30
 
31
+ def apply_headers(request)
32
+ request["User-Agent"] = @configuration.user_agent
33
+ request["Accept"] = "application/json"
34
+ end
35
+
31
36
  def build_uri(path, params)
32
37
  full_url = "#{@base_url}#{path}"
33
38
  uri = URI.parse(full_url)
@@ -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
@@ -4,17 +4,18 @@ module Uk
4
4
  module HsCode
5
5
  module Parsers
6
6
  class SearchResponseParser
7
- def self.call(response_hash)
8
- new(response_hash).call
7
+ def self.call(response_hash, query: nil)
8
+ new(response_hash, query: query).call
9
9
  end
10
10
 
11
- def initialize(response_hash)
11
+ def initialize(response_hash, query: nil)
12
12
  @response_hash = response_hash || {}
13
+ @query = query
13
14
  end
14
15
 
15
16
  def call
16
17
  data = @response_hash["data"] || @response_hash[:data]
17
- return [] unless data
18
+ return Entities::SearchResult.new([], query: @query) unless data
18
19
 
19
20
  type = data["type"] || data[:type]
20
21
  attributes = data["attributes"] || data[:attributes] || {}
@@ -25,7 +26,7 @@ module Uk
25
26
  when "fuzzy_search"
26
27
  parse_fuzzy_search(attributes)
27
28
  else
28
- []
29
+ Entities::SearchResult.new([], query: @query, type: type.to_s)
29
30
  end
30
31
  end
31
32
 
@@ -34,65 +35,74 @@ module Uk
34
35
  def parse_exact_search(attributes)
35
36
  entry = attributes["entry"] || attributes[:entry] || {}
36
37
  code = entry["id"] || entry[:id]
37
- return [] unless code
38
+ endpoint = entry["endpoint"] || entry[:endpoint] || "commodities"
39
+ return Entities::SearchResult.new([], query: @query, type: "exact_match") unless code
38
40
 
39
- [
40
- Record.new(
41
- code: code,
42
- title: "Exact match for #{code}",
43
- declarable: true,
44
- type: "exact"
45
- )
46
- ]
41
+ exact = Entities::ExactMatch.new(code: code, endpoint: endpoint)
42
+ Entities::SearchResult.new([exact], query: @query, type: "exact_match", exact_match: exact)
47
43
  end
48
44
 
49
45
  def parse_fuzzy_search(attributes)
50
- records = []
51
- seen_codes = {}
46
+ commodities = extract_commodities(attributes)
47
+ references = extract_references(attributes)
48
+ records = (commodities + references).uniq(&:code)
52
49
 
53
- extract_commodities(attributes, records, seen_codes)
54
- extract_references(attributes, records, seen_codes)
55
-
56
- records
50
+ Entities::SearchResult.new(
51
+ records,
52
+ query: @query,
53
+ type: "fuzzy_match",
54
+ commodities: commodities,
55
+ references: references
56
+ )
57
57
  end
58
58
 
59
- def extract_commodities(attributes, records, seen_codes)
59
+ def extract_commodities(attributes)
60
+ commodities = []
60
61
  gn_commodities = attributes.dig("goods_nomenclature_match", "commodities") || []
62
+
61
63
  gn_commodities.each do |hit|
62
64
  source = hit["_source"] || {}
63
65
  code = source["goods_nomenclature_item_id"]
64
- next if code.nil? || seen_codes[code]
66
+ next if code.nil?
65
67
 
66
- seen_codes[code] = true
67
- records << Record.new(
68
+ commodities << Entities::Commodity.new(
68
69
  code: code,
69
- title: source["description"],
70
70
  description: source["description"],
71
71
  score: hit["_score"],
72
72
  declarable: source["declarable"],
73
- type: "commodity"
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"]
74
79
  )
75
80
  end
81
+
82
+ commodities
76
83
  end
77
84
 
78
- def extract_references(attributes, records, seen_codes)
85
+ def extract_references(attributes)
86
+ references = []
79
87
  ref_commodities = attributes.dig("reference_match", "commodities") || []
88
+
80
89
  ref_commodities.each do |hit|
81
90
  source = hit["_source"] || {}
82
91
  ref_data = source["reference"] || {}
83
92
  code = ref_data["goods_nomenclature_item_id"]
84
- next if code.nil? || seen_codes[code]
93
+ next if code.nil?
85
94
 
86
- seen_codes[code] = true
87
- records << Record.new(
95
+ references << Entities::SearchReference.new(
88
96
  code: code,
89
97
  title: source["title"],
90
98
  description: ref_data["description"] || source["title"],
91
99
  score: hit["_score"],
92
100
  declarable: ref_data["declarable"],
93
- type: "reference"
101
+ reference_class: source["reference_class"] || "Commodity"
94
102
  )
95
103
  end
104
+
105
+ references
96
106
  end
97
107
  end
98
108
  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
@@ -27,7 +27,7 @@ module Uk
27
27
 
28
28
  def execute_workflow
29
29
  raw_response = @client.get("/search", { q: query })
30
- Parsers::SearchResponseParser.call(raw_response)
30
+ Parsers::SearchResponseParser.call(raw_response, query: query)
31
31
  end
32
32
 
33
33
  def query
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Uk
4
4
  module HsCode
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
data/lib/uk/hs_code.rb CHANGED
@@ -1,10 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "hs_code/version"
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"
4
9
  require_relative "hs_code/record"
5
10
  require_relative "hs_code/client"
6
11
  require_relative "hs_code/parsers/search_response_parser"
7
12
  require_relative "hs_code/services/search_service"
13
+ require_relative "hs_code/services/search_references_service"
8
14
  require_relative "hs_code/api_request"
9
15
 
10
16
  module Uk
@@ -14,6 +20,18 @@ module Uk
14
20
  class Error < StandardError; end
15
21
 
16
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
+
17
35
  def search(query)
18
36
  Services::SearchService.call(query: query)
19
37
  end
@@ -23,7 +41,7 @@ module Uk
23
41
  end
24
42
 
25
43
  def search_references(query)
26
- ApiRequest.new("#{API_ENDPOINT}/search_references.json", { query: { letter: query } }).get
44
+ Services::SearchReferencesService.call(query: query)
27
45
  end
28
46
  end
29
47
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: uk-hs_code
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mateusz Michalski
@@ -125,8 +125,14 @@ files:
125
125
  - lib/uk/hs_code.rb
126
126
  - lib/uk/hs_code/api_request.rb
127
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
128
133
  - lib/uk/hs_code/parsers/search_response_parser.rb
129
134
  - lib/uk/hs_code/record.rb
135
+ - lib/uk/hs_code/services/search_references_service.rb
130
136
  - lib/uk/hs_code/services/search_service.rb
131
137
  - lib/uk/hs_code/version.rb
132
138
  - sig/uk/hs_code.rbs