dpd_ie_api 0.1.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: 9533f8df5f9307ad248f427201256fa16a7aa3bd5bcfcdc9190719766c4a098b
4
- data.tar.gz: 2c6d9ed162e0bd9bf499e1d5502610075cc948f6a9b1422c6dd03878cdc17787
3
+ metadata.gz: 6b5b88bb8e13571ef8c0c054f91249ce27b2e8b96c150502f7c221857f3ee8ee
4
+ data.tar.gz: bd6ca4477ced7a57505a6160505a9f21da1a375745bed39aeb62b64265f0a58c
5
5
  SHA512:
6
- metadata.gz: d05b4759df9ced94d34e933c4e0c9cf385aaa133fea6081b3c860253e113e533615acc3a203db9f64766382a07a4f2fdef0513373d752ca2e94b21686a328bb3
7
- data.tar.gz: 3ddf416e0abcb89d4a8b50bc79eedf1195854174b15982746177b61cd2d5002cc1c27cfe1aa08afcfb75e32d2bd1191e6ebfb9702b0ebd217ddc79a88a9f8fd9
6
+ metadata.gz: d0d6555fc183eae6676c651aab5c4a2c31767db9d83cd071c2604d88c15a7a1f831bc5cbe16601570f8ba46e5d185cb47fb886c095f17b71a4ecbec57f6ab826
7
+ data.tar.gz: 1a95e8bbcd680bfba8759a01ca8b20d2cea89f75156a6b5bfe9bf71c3d86ed784ddc0ff39166ee51967932635469af7e7b61e3a1595343dd85f9dfaf4ddb78d5
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/README.md CHANGED
@@ -1,34 +1,226 @@
1
1
  # DpdIeApi
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
4
-
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/dpd_ie_api`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ A Ruby gem wrapper for the DPD Ireland API.
6
4
 
7
5
  ## Installation
8
6
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
10
-
11
7
  Install the gem and add to the application's Gemfile by executing:
12
8
 
13
- $ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
9
+ ```bash
10
+ bundle add dpd_ie_api
11
+ ```
14
12
 
15
13
  If bundler is not being used to manage dependencies, install the gem by executing:
16
14
 
17
- $ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
15
+ ```bash
16
+ gem install dpd_ie_api
17
+ ```
18
+
19
+ ## Design
20
+
21
+ The gem provides a **uniform Ruby interface** across all endpoints. You always work with Ruby hashes and objects — the gem picks the best format (JSON or XML) for each endpoint internally:
22
+
23
+ | Endpoint | Request Format | Response Format |
24
+ | --------- | -------------- | --------------- |
25
+ | Authorize | JSON | JSON |
26
+ | PreAdvice | XML | XML |
27
+ | Track | XML | JSON |
28
+ | Locator | JSON | JSON |
29
+
30
+ JSON is used wherever the DPD API supports it (simpler, no XML parsing overhead). XML is used only where the API requires it. This is entirely invisible to gem consumers.
18
31
 
19
32
  ## Usage
20
33
 
21
- TODO: Write usage instructions here
34
+ ### Authentication
35
+
36
+ Obtain an access token using your static token and credentials.
37
+
38
+ ```ruby
39
+ auth = DpdIeApi::Auth.fetch_token("YOUR_STATIC_TOKEN", "YOUR_USER", "YOUR_PASSWORD")
40
+ access_token = auth.access_token
41
+ refresh_token = auth.refresh_token
42
+ expires_in = auth.expires_in
43
+ ```
44
+
45
+ > [!IMPORTANT]
46
+ > The access token expires after a certain period (indicated by `expires_in`). It is highly recommended to cache the `access_token` and reuse it until it expires to avoid unnecessary API calls and potential rate limiting.
47
+
48
+ For production, pass `prod: true`:
49
+
50
+ ```ruby
51
+ auth = DpdIeApi::Auth.fetch_token("YOUR_STATIC_TOKEN", "YOUR_USER", "YOUR_PASSWORD", prod: true)
52
+ ```
53
+
54
+ Then, initialize the client with your access token:
55
+
56
+ ```ruby
57
+ client = DpdIeApi::Client.new(
58
+ access_token,
59
+ prod: true # set to true for production, false for pre-production (default)
60
+ )
61
+ ```
62
+
63
+ **Client constructor:**
64
+
65
+ - `access_token` - Bearer token from `DpdIeApi::Auth.fetch_token` (required)
66
+ - `prod:` - Set to `true` for production (`https://papi.dpd.ie`), `false` for pre-production (`https://pre-prod-papi.dpd.ie`). Defaults to `false`.
67
+
68
+ ### Error Handling
69
+
70
+ All endpoints raise `DpdIeApi::Error` on failure. The error object exposes both HTTP status code and response body:
71
+
72
+ ```ruby
73
+ begin
74
+ client.preadvice.create(payload)
75
+ rescue DpdIeApi::Error => error
76
+ puts error.message
77
+ puts error.response_http_code
78
+ puts error.response_body
79
+ end
80
+ ```
81
+
82
+ ### Preadvice
83
+
84
+ #### Create
85
+
86
+ Create a preadvice consignment. Pass a Ruby hash — the gem handles XML serialization internally.
87
+
88
+ ```ruby
89
+ payload = {
90
+ consignment: {
91
+ record_id: '1',
92
+ customer_account: '5993L3',
93
+ total_parcels: '1',
94
+ relabel: '1',
95
+ service_option: '5',
96
+ service_type: '1',
97
+ weight: '5',
98
+ delivery_address: {
99
+ contact: 'Warehouse address',
100
+ contact_telephone: '0877777777',
101
+ contact_email: 'test.email@no-email.gg',
102
+ business_name: 'Shop name',
103
+ address_line1: 'Trimgate St',
104
+ address_line2: 'Athlone Business Park',
105
+ address_line3: 'Navan',
106
+ address_line4: 'Meath',
107
+ post_code: 'N37XK83',
108
+ country_code: 'IE',
109
+ },
110
+ collection_address: {
111
+ contact: 'Customer address',
112
+ contact_telephone: '0877777777',
113
+ contact_email: 'test.email@no-email.gg',
114
+ business_name: 'Customer name',
115
+ address_line1: '40 Drumcondra Rd Lower',
116
+ address_line2: '',
117
+ address_line3: 'Drumcondra',
118
+ address_line4: 'Dublin',
119
+ post_code: 'N37XK83',
120
+ country_code: 'IE'
121
+ },
122
+ references: [
123
+ { reference_name: 'Reference1', reference_value: 'Order Name 123', parcel_number: '1' },
124
+ ]
125
+ }
126
+ }
127
+
128
+ result = client.preadvice.create(payload)
129
+ puts result.status
130
+ puts result.consignment.tracking_number
131
+ puts result.consignment.label_image
132
+ ```
133
+
134
+ > [!NOTE]
135
+ > Hash keys can be symbols (snake_case, auto-converted to PascalCase XML elements) or strings (passed through as-is for exact control). For example, use `"RecordID" => '1'` if the API requires a specific casing that differs from the automatic conversion.
136
+
137
+ ### Track
138
+
139
+ #### Find
140
+
141
+ Retrieve tracking information for a consignment.
142
+
143
+ ```ruby
144
+ tracking = client.track.find("800436802")
145
+
146
+ puts tracking.tracking_number
147
+ puts tracking.consignment_number
148
+ puts tracking.consignment_status
149
+ puts tracking.consignment_service
150
+
151
+ # Tracking history events
152
+ event = tracking.tracking_history.event_detail
153
+ puts event.event_date
154
+ puts event.event_time
155
+ puts event.event_type_name
156
+ puts event.event_depot_name
157
+ ```
158
+
159
+ The `tracking_type` defaults to `"consignment"` but can be overridden:
160
+
161
+ ```ruby
162
+ tracking = client.track.find("800436802", tracking_type: "parcel")
163
+ ```
164
+
165
+ ### Locator
166
+
167
+ #### List
168
+
169
+ Retrieve DPD Ireland drop-off locations. Test and deactivated locations are automatically filtered out.
170
+
171
+ ```ruby
172
+ locations = client.locator.list
173
+
174
+ locations.each do |location|
175
+ puts location.location_name
176
+ puts location.location_type # "Pickup" or "Depot" or "Locker"
177
+ puts location.location_id
178
+ puts location.location_address_line1
179
+ puts location.location_code
180
+ puts location.county
181
+ puts location.country
182
+ puts location.gps_lat
183
+ puts location.gps_long
184
+ puts location.depot_no
185
+ puts location.province
186
+ puts location.facilities
187
+ puts location.group_pudo_id
188
+ end
189
+ ```
190
+
191
+ Filter by type or specific location ID:
192
+
193
+ ```ruby
194
+ # Only depots
195
+ locations = client.locator.list(type: "Depot")
196
+
197
+ # Only pickups
198
+ locations = client.locator.list(type: "Pickup")
199
+
200
+ # Only lockers
201
+ locations = client.locator.list(type: "Locker")
202
+
203
+ # Specific location
204
+ locations = client.locator.list(location_id: "5308")
205
+ ```
206
+
207
+ ### Response Objects
208
+
209
+ All endpoints return response objects that provide:
210
+
211
+ - **Dot notation access** — `result.consignment.tracking_number` (keys are auto-underscored)
212
+ - **`#raw`** — the original parsed response hash with original PascalCase keys
213
+ - **`#to_hash`** — convert the response object back to a Ruby hash
22
214
 
23
215
  ## Development
24
216
 
25
- After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
217
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
26
218
 
27
219
  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).
28
220
 
29
221
  ## Contributing
30
222
 
31
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/dpd_ie_api. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/dpd_ie_api/blob/main/CODE_OF_CONDUCT.md).
223
+ Bug reports and pull requests are welcome on GitHub at https://github.com/postco/dpd_ie_api. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/postco/dpd_ie_api/blob/main/CODE_OF_CONDUCT.md).
32
224
 
33
225
  ## License
34
226
 
@@ -36,4 +228,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
36
228
 
37
229
  ## Code of Conduct
38
230
 
39
- Everyone interacting in the DpdIeApi project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/dpd_ie_api/blob/main/CODE_OF_CONDUCT.md).
231
+ Everyone interacting in the DpdIeApi project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/postco/dpd_ie_api/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile CHANGED
@@ -1,4 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'bundler/gem_tasks'
4
- task default: %i[]
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'json'
5
+
6
+ module DpdIeApi
7
+ class Auth
8
+ PROD_BASE_URL = 'https://papi.dpd.ie'
9
+ TEST_BASE_URL = 'https://pre-prod-papi.dpd.ie'
10
+ AUTH_PATH = '/common/api/authorize'
11
+
12
+ def self.fetch_token(static_token, user, password, prod: false)
13
+ base_url = prod ? PROD_BASE_URL : TEST_BASE_URL
14
+ url = "#{base_url}#{AUTH_PATH}"
15
+
16
+ response = Faraday.post(url) do |req|
17
+ req.headers['Authorization'] = "Basic #{static_token}"
18
+ req.headers['Content-Type'] = 'application/json'
19
+ req.headers['Accept'] = 'application/json'
20
+ req.body = { 'User' => user, 'Password' => password, 'Type' => 'CUST' }.to_json
21
+ end
22
+
23
+ unless response.success?
24
+ raise DpdIeApi::Error.new(
25
+ "Failed to authorize: #{response.status} - #{response.body}",
26
+ response_http_code: response.status,
27
+ response_body: response.body
28
+ )
29
+ end
30
+
31
+ parsed = JSON.parse(response.body)
32
+
33
+ if parsed['Status'] == 'FAIL'
34
+ raise DpdIeApi::Error.new(
35
+ "Authorization error: #{parsed['Reason']}",
36
+ response_http_code: response.status,
37
+ response_body: response.body
38
+ )
39
+ end
40
+
41
+ Objects::Auth.new(parsed)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+
5
+ module DpdIeApi
6
+ class Client
7
+ PROD_BASE_URL = 'https://papi.dpd.ie'
8
+ TEST_BASE_URL = 'https://pre-prod-papi.dpd.ie'
9
+
10
+ def initialize(access_token, prod: false)
11
+ @access_token = access_token
12
+ @prod = prod
13
+ end
14
+
15
+ def base_url
16
+ @prod ? PROD_BASE_URL : TEST_BASE_URL
17
+ end
18
+
19
+ def connection
20
+ @connection ||= Faraday.new(url: base_url) do |f|
21
+ f.headers['Authorization'] = "Bearer #{@access_token}"
22
+ f.headers['Content-Type'] = 'application/xml'
23
+ end
24
+ end
25
+
26
+ def preadvice
27
+ Resources::Preadvice.new(self)
28
+ end
29
+
30
+ def track
31
+ Resources::Track.new(self)
32
+ end
33
+
34
+ def locator
35
+ Resources::Locator.new(self)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DpdIeApi
4
+ module Objects
5
+ class Auth < Base
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_support/core_ext/string'
5
+ require 'ostruct'
6
+
7
+ module DpdIeApi
8
+ module Objects
9
+ class Base < OpenStruct
10
+ attr_reader :original_response
11
+
12
+ def initialize(attributes)
13
+ @original_response = deep_freeze_object(attributes)
14
+ super(to_ostruct(attributes))
15
+ end
16
+
17
+ def to_ostruct(obj)
18
+ if obj.is_a?(Hash)
19
+ OpenStruct.new(obj.transform_keys { |key| key.to_s.underscore }.transform_values { |val| to_ostruct(val) })
20
+ elsif obj.is_a?(Array)
21
+ obj.map { |o| to_ostruct(o) }
22
+ else
23
+ obj
24
+ end
25
+ end
26
+
27
+ def to_hash
28
+ ostruct_to_hash(self)
29
+ end
30
+
31
+ def raw
32
+ @original_response
33
+ end
34
+
35
+ private
36
+
37
+ def deep_freeze_object(obj)
38
+ case obj
39
+ when Hash
40
+ obj.transform_values { |value| deep_freeze_object(value) }.freeze
41
+ when Array
42
+ obj.map { |item| deep_freeze_object(item) }.freeze
43
+ else
44
+ obj.respond_to?(:freeze) ? obj.freeze : obj
45
+ end
46
+ end
47
+
48
+ def ostruct_to_hash(object)
49
+ case object
50
+ when OpenStruct
51
+ hash = object.to_h.except(:table)
52
+ hash.transform_keys(&:to_s).transform_values { |value| ostruct_to_hash(value) }
53
+ when Array
54
+ object.map { |item| ostruct_to_hash(item) }
55
+ when Hash
56
+ object.transform_keys(&:to_s).transform_values { |value| ostruct_to_hash(value) }
57
+ else
58
+ object
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DpdIeApi
4
+ module Objects
5
+ class Locator < Base
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DpdIeApi
4
+ module Objects
5
+ class Preadvice < Base
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DpdIeApi
4
+ module Objects
5
+ class Track < Base
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module DpdIeApi
6
+ module Resources
7
+ class Locator
8
+ LOCATOR_PATH = '/common/api/dpdlocation'
9
+
10
+ def initialize(client)
11
+ @client = client
12
+ end
13
+
14
+ def list(type: nil, location_id: nil)
15
+ response = post_request(type, location_id)
16
+ parsed = parse_response(response)
17
+ filter_locations(parsed)
18
+ end
19
+
20
+ private
21
+
22
+ def post_request(type, location_id)
23
+ response = @client.connection.post(LOCATOR_PATH) do |req|
24
+ req.headers['Content-Type'] = 'application/json'
25
+ req.headers['Accept'] = 'application/json'
26
+ req.body = {
27
+ 'CountryCode' => 'IE',
28
+ 'Type' => type.to_s,
29
+ 'LocationID' => location_id.to_s
30
+ }.to_json
31
+ end
32
+
33
+ unless response.success?
34
+ raise DpdIeApi::Error.new(
35
+ "Failed to fetch locations: #{response.status} - #{response.body}",
36
+ response_http_code: response.status,
37
+ response_body: response.body
38
+ )
39
+ end
40
+
41
+ response
42
+ end
43
+
44
+ def parse_response(response)
45
+ parsed = JSON.parse(response.body)
46
+
47
+ if parsed['Status'] != 'OK'
48
+ raise DpdIeApi::Error.new(
49
+ "Locator error: #{parsed['Status']}",
50
+ response_http_code: response.status,
51
+ response_body: response.body
52
+ )
53
+ end
54
+
55
+ parsed
56
+ end
57
+
58
+ def filter_locations(parsed)
59
+ locations = parsed['Location']
60
+ locations = [locations] if locations.is_a?(Hash)
61
+ locations = [] if locations.nil?
62
+
63
+ locations
64
+ .reject { |loc| loc['locationName']&.match?(/test|do not reactivate/i) }
65
+ .map { |loc| Objects::Locator.new(loc) }
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DpdIeApi
4
+ module Resources
5
+ class Preadvice
6
+ PREADVICE_PATH = '/common/api/preadvice'
7
+
8
+ def initialize(client)
9
+ @client = client
10
+ end
11
+
12
+ def create(payload)
13
+ xml_body = DpdIeApi::XmlBuilder.build('PreAdvice', payload)
14
+
15
+ response = @client.connection.post(PREADVICE_PATH) do |req|
16
+ req.body = xml_body
17
+ end
18
+
19
+ unless response.success?
20
+ raise DpdIeApi::Error.new(
21
+ "Failed to create preadvice: #{response.status} - #{response.body}",
22
+ response_http_code: response.status,
23
+ response_body: response.body
24
+ )
25
+ end
26
+
27
+ parsed = DpdIeApi::XmlParser.parse(response.body, '//PreAdviceResponse')
28
+
29
+ if parsed['Status'] == 'FAIL'
30
+ raise DpdIeApi::Error.new(
31
+ "Preadvice error: #{parsed['PreAdviceErrorDetails']}",
32
+ response_http_code: response.status,
33
+ response_body: response.body
34
+ )
35
+ end
36
+
37
+ Objects::Preadvice.new(parsed)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module DpdIeApi
6
+ module Resources
7
+ class Track
8
+ TRACK_PATH = '/common/api/track'
9
+
10
+ def initialize(client)
11
+ @client = client
12
+ end
13
+
14
+ def find(tracking_number, tracking_type: 'consignment')
15
+ xml_body = build_tracking_xml(tracking_number, tracking_type)
16
+
17
+ response = @client.connection.post(TRACK_PATH) do |req|
18
+ req.headers['Accept'] = 'application/json'
19
+ req.body = xml_body
20
+ end
21
+
22
+ unless response.success?
23
+ raise DpdIeApi::Error.new(
24
+ "Failed to track consignment: #{response.status} - #{response.body}",
25
+ response_http_code: response.status,
26
+ response_body: response.body
27
+ )
28
+ end
29
+
30
+ parsed = JSON.parse(response.body)
31
+
32
+ if parsed['TrackingErrorInfo']
33
+ error_info = unwrap(parsed['TrackingErrorInfo'])
34
+ error_detail = unwrap(error_info['TrackingErrorDetail'])
35
+ raise DpdIeApi::Error.new(
36
+ "Tracking error: #{error_detail['ErrorDetailCodeDesc']}",
37
+ response_http_code: response.status,
38
+ response_body: response.body
39
+ )
40
+ end
41
+
42
+ Objects::Track.new(unwrap(parsed['TrackingInfo']))
43
+ end
44
+
45
+ private
46
+
47
+ def unwrap(value)
48
+ value.is_a?(Array) ? value.first : value
49
+ end
50
+
51
+ def build_tracking_xml(tracking_number, tracking_type)
52
+ DpdIeApi::XmlBuilder.build('TrackingRequest', {
53
+ request_line: {
54
+ tracking_number: tracking_number,
55
+ tracking_type: tracking_type
56
+ }
57
+ })
58
+ end
59
+ end
60
+ end
61
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DpdIeApi
4
- VERSION = "0.1.0"
4
+ VERSION = '0.3.0'
5
5
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nokogiri'
4
+ require 'active_support/core_ext/string/inflections'
5
+
6
+ module DpdIeApi
7
+ class XmlBuilder
8
+ def self.build(root_name, data)
9
+ builder = Nokogiri::XML::Builder.new(encoding: 'iso-8859-1') do |xml|
10
+ xml.send(root_name) do
11
+ hash_to_xml(xml, data)
12
+ end
13
+ end
14
+ builder.to_xml
15
+ end
16
+
17
+ class << self
18
+ private
19
+
20
+ def hash_to_xml(xml, data)
21
+ data.each do |key, value|
22
+ element_name = normalize_key(key)
23
+
24
+ case value
25
+ when Hash
26
+ xml.send(element_name) do
27
+ hash_to_xml(xml, value)
28
+ end
29
+ when Array
30
+ xml.send(element_name) do
31
+ child_name = element_name.singularize
32
+ value.each do |item|
33
+ if item.is_a?(Hash)
34
+ xml.send(child_name) do
35
+ hash_to_xml(xml, item)
36
+ end
37
+ else
38
+ xml.send(child_name, item.to_s)
39
+ end
40
+ end
41
+ end
42
+ else
43
+ xml.send(element_name, value.to_s)
44
+ end
45
+ end
46
+ end
47
+
48
+ def normalize_key(key)
49
+ case key
50
+ when Symbol then key.to_s.camelize
51
+ when String then key
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nokogiri'
4
+
5
+ module DpdIeApi
6
+ class XmlParser
7
+ def self.parse(body, root_xpath)
8
+ doc = Nokogiri::XML(body)
9
+ root = doc.at_xpath(root_xpath)
10
+ xml_node_to_hash(root)
11
+ end
12
+
13
+ class << self
14
+ private
15
+
16
+ def xml_node_to_hash(node)
17
+ return nil if node.nil?
18
+
19
+ children = node.element_children
20
+
21
+ return node.text if children.empty?
22
+
23
+ result = {}
24
+ children.each do |child|
25
+ key = child.name
26
+ if result.key?(key)
27
+ result[key] = [result[key]] unless result[key].is_a?(Array)
28
+ result[key] << xml_node_to_hash(child)
29
+ else
30
+ result[key] = xml_node_to_hash(child)
31
+ end
32
+ end
33
+ result
34
+ end
35
+ end
36
+ end
37
+ end
data/lib/dpd_ie_api.rb CHANGED
@@ -1,8 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "dpd_ie_api/version"
3
+ require_relative 'dpd_ie_api/version'
4
+ require_relative 'dpd_ie_api/objects/base'
5
+ require_relative 'dpd_ie_api/objects/auth'
6
+ require_relative 'dpd_ie_api/objects/preadvice'
7
+ require_relative 'dpd_ie_api/objects/track'
8
+ require_relative 'dpd_ie_api/objects/locator'
9
+ require_relative 'dpd_ie_api/xml_builder'
10
+ require_relative 'dpd_ie_api/xml_parser'
11
+ require_relative 'dpd_ie_api/auth'
12
+ require_relative 'dpd_ie_api/client'
13
+ require_relative 'dpd_ie_api/resources/preadvice'
14
+ require_relative 'dpd_ie_api/resources/track'
15
+ require_relative 'dpd_ie_api/resources/locator'
4
16
 
5
17
  module DpdIeApi
6
- class Error < StandardError; end
7
- # Your code goes here...
18
+ class Error < StandardError
19
+ attr_reader :response_http_code, :response_body
20
+
21
+ def initialize(message = nil, response_http_code: nil, response_body: nil)
22
+ @response_http_code = response_http_code
23
+ @response_body = response_body
24
+ super(message)
25
+ end
26
+ end
8
27
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dpd_ie_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Linh Ho
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-03-13 00:00:00.000000000 Z
11
+ date: 2026-03-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: nokogiri
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: activesupport
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +66,20 @@ dependencies:
52
66
  - - "~>"
53
67
  - !ruby/object:Gem::Version
54
68
  version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
55
83
  description: Provides a Ruby interface for interacting with the DPD Ireland API, including
56
84
  authorization, label generation, and tracking.
57
85
  email:
@@ -60,22 +88,35 @@ executables: []
60
88
  extensions: []
61
89
  extra_rdoc_files: []
62
90
  files:
91
+ - ".rspec"
63
92
  - CHANGELOG.md
64
93
  - CODE_OF_CONDUCT.md
65
94
  - LICENSE.txt
66
95
  - README.md
67
96
  - Rakefile
68
97
  - lib/dpd_ie_api.rb
98
+ - lib/dpd_ie_api/auth.rb
99
+ - lib/dpd_ie_api/client.rb
100
+ - lib/dpd_ie_api/objects/auth.rb
101
+ - lib/dpd_ie_api/objects/base.rb
102
+ - lib/dpd_ie_api/objects/locator.rb
103
+ - lib/dpd_ie_api/objects/preadvice.rb
104
+ - lib/dpd_ie_api/objects/track.rb
105
+ - lib/dpd_ie_api/resources/locator.rb
106
+ - lib/dpd_ie_api/resources/preadvice.rb
107
+ - lib/dpd_ie_api/resources/track.rb
69
108
  - lib/dpd_ie_api/version.rb
109
+ - lib/dpd_ie_api/xml_builder.rb
110
+ - lib/dpd_ie_api/xml_parser.rb
70
111
  - sig/dpd_ie_api.rbs
71
- homepage: https://github.com/hplinh0702/dpd_ie_api
112
+ homepage: https://github.com/postco/dpd_ie_api
72
113
  licenses:
73
114
  - MIT
74
115
  metadata:
75
116
  allowed_push_host: https://rubygems.org
76
- homepage_uri: https://github.com/hplinh0702/dpd_ie_api
77
- source_code_uri: https://github.com/hplinh0702/dpd_ie_api
78
- changelog_uri: https://github.com/hplinh0702/dpd_ie_api/blob/main/CHANGELOG.md
117
+ homepage_uri: https://github.com/postco/dpd_ie_api
118
+ source_code_uri: https://github.com/postco/dpd_ie_api
119
+ changelog_uri: https://github.com/postco/dpd_ie_api/blob/main/CHANGELOG.md
79
120
  post_install_message:
80
121
  rdoc_options: []
81
122
  require_paths: