speedy-bg 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.
Files changed (148) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +3 -0
  3. data/CODE_OF_CONDUCT.md +134 -0
  4. data/CONTRIBUTING.md +67 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +150 -0
  7. data/SECURITY.md +29 -0
  8. data/docs/API.md +740 -0
  9. data/docs/RESPONSE_CONTRACTS.md +55 -0
  10. data/docs/VCR.md +161 -0
  11. data/docs/schema-notes.md +38 -0
  12. data/lib/speedy/camel_case_middleware.rb +71 -0
  13. data/lib/speedy/client.rb +100 -0
  14. data/lib/speedy/client_resources.rb +776 -0
  15. data/lib/speedy/collection.rb +58 -0
  16. data/lib/speedy/error.rb +29 -0
  17. data/lib/speedy/model.rb +92 -0
  18. data/lib/speedy/models/calculation/address_location.rb +16 -0
  19. data/lib/speedy/models/calculation/calculation_content.rb +15 -0
  20. data/lib/speedy/models/calculation/calculation_recipient.rb +15 -0
  21. data/lib/speedy/models/calculation/calculation_result.rb +19 -0
  22. data/lib/speedy/models/calculation/calculation_sender.rb +16 -0
  23. data/lib/speedy/models/calculation/calculation_service.rb +16 -0
  24. data/lib/speedy/models/clients/client.rb +21 -0
  25. data/lib/speedy/models/clients/cod_contract_info.rb +16 -0
  26. data/lib/speedy/models/clients/cod_international_annex.rb +12 -0
  27. data/lib/speedy/models/clients/contract_info.rb +16 -0
  28. data/lib/speedy/models/clients/requirement.rb +12 -0
  29. data/lib/speedy/models/clients/special_delivery_requirements.rb +12 -0
  30. data/lib/speedy/models/common/address.rb +36 -0
  31. data/lib/speedy/models/error.rb +37 -0
  32. data/lib/speedy/models/location/address_nomenclature_type.rb +12 -0
  33. data/lib/speedy/models/location/block.rb +15 -0
  34. data/lib/speedy/models/location/complex.rb +23 -0
  35. data/lib/speedy/models/location/country.rb +25 -0
  36. data/lib/speedy/models/location/find_office_response.rb +12 -0
  37. data/lib/speedy/models/location/find_site_response.rb +12 -0
  38. data/lib/speedy/models/location/get_office_response.rb +12 -0
  39. data/lib/speedy/models/location/get_site_response.rb +12 -0
  40. data/lib/speedy/models/location/office.rb +41 -0
  41. data/lib/speedy/models/location/office_result.rb +13 -0
  42. data/lib/speedy/models/location/office_working_time_schedule.rb +15 -0
  43. data/lib/speedy/models/location/point_of_interest.rb +22 -0
  44. data/lib/speedy/models/location/postcode.rb +12 -0
  45. data/lib/speedy/models/location/routing_information.rb +14 -0
  46. data/lib/speedy/models/location/site.rb +30 -0
  47. data/lib/speedy/models/location/state.rb +17 -0
  48. data/lib/speedy/models/location/street.rb +23 -0
  49. data/lib/speedy/models/location/tour.rb +14 -0
  50. data/lib/speedy/models/payments/payout.rb +20 -0
  51. data/lib/speedy/models/payments/payout_details.rb +23 -0
  52. data/lib/speedy/models/pickup/pickup_order.rb +16 -0
  53. data/lib/speedy/models/pickup/terms_response.rb +12 -0
  54. data/lib/speedy/models/print/extended_print_response.rb +13 -0
  55. data/lib/speedy/models/print/label_info.rb +19 -0
  56. data/lib/speedy/models/print/label_info_response.rb +12 -0
  57. data/lib/speedy/models/print/parcel_to_print_additional_barcode.rb +14 -0
  58. data/lib/speedy/models/print/print_response.rb +12 -0
  59. data/lib/speedy/models/response.rb +13 -0
  60. data/lib/speedy/models/services/additional_courier_service.rb +11 -0
  61. data/lib/speedy/models/services/additional_courier_services.rb +21 -0
  62. data/lib/speedy/models/services/courier_service.rb +19 -0
  63. data/lib/speedy/models/services/extended_courier_service.rb +13 -0
  64. data/lib/speedy/models/shipments/bank_account.rb +12 -0
  65. data/lib/speedy/models/shipments/barcode_information.rb +20 -0
  66. data/lib/speedy/models/shipments/cod_payment.rb +12 -0
  67. data/lib/speedy/models/shipments/content.rb +20 -0
  68. data/lib/speedy/models/shipments/create_shipment_response.rb +16 -0
  69. data/lib/speedy/models/shipments/created_shipment_parcel.rb +14 -0
  70. data/lib/speedy/models/shipments/external_carrier_parcel_number.rb +12 -0
  71. data/lib/speedy/models/shipments/goods_item.rb +16 -0
  72. data/lib/speedy/models/shipments/money_transfer_premium.rb +13 -0
  73. data/lib/speedy/models/shipments/parcel.rb +22 -0
  74. data/lib/speedy/models/shipments/payment.rb +16 -0
  75. data/lib/speedy/models/shipments/primary_shipment.rb +12 -0
  76. data/lib/speedy/models/shipments/recipient.rb +21 -0
  77. data/lib/speedy/models/shipments/return_amounts.rb +11 -0
  78. data/lib/speedy/models/shipments/secondary_shipment.rb +18 -0
  79. data/lib/speedy/models/shipments/sender.rb +21 -0
  80. data/lib/speedy/models/shipments/shipment.rb +28 -0
  81. data/lib/speedy/models/shipments/shipment_additional_services.rb +17 -0
  82. data/lib/speedy/models/shipments/shipment_address.rb +10 -0
  83. data/lib/speedy/models/shipments/shipment_cod_additional_service.rb +20 -0
  84. data/lib/speedy/models/shipments/shipment_cod_fiscal_receipt_item.rb +14 -0
  85. data/lib/speedy/models/shipments/shipment_content.rb +24 -0
  86. data/lib/speedy/models/shipments/shipment_declared_value_additional_service.rb +13 -0
  87. data/lib/speedy/models/shipments/shipment_delivery.rb +14 -0
  88. data/lib/speedy/models/shipments/shipment_discount_card_id.rb +12 -0
  89. data/lib/speedy/models/shipments/shipment_electronic_return_receipt_additional_service.rb +14 -0
  90. data/lib/speedy/models/shipments/shipment_obpd.rb +13 -0
  91. data/lib/speedy/models/shipments/shipment_parcel.rb +23 -0
  92. data/lib/speedy/models/shipments/shipment_parcel_number.rb +12 -0
  93. data/lib/speedy/models/shipments/shipment_parcel_ref.rb +13 -0
  94. data/lib/speedy/models/shipments/shipment_parcel_size.rb +13 -0
  95. data/lib/speedy/models/shipments/shipment_payment.rb +17 -0
  96. data/lib/speedy/models/shipments/shipment_phone_number.rb +12 -0
  97. data/lib/speedy/models/shipments/shipment_price.rb +23 -0
  98. data/lib/speedy/models/shipments/shipment_price_amount.rb +13 -0
  99. data/lib/speedy/models/shipments/shipment_recipient.rb +14 -0
  100. data/lib/speedy/models/shipments/shipment_return_additional_services.rb +20 -0
  101. data/lib/speedy/models/shipments/shipment_return_receipt_additional_service.rb +14 -0
  102. data/lib/speedy/models/shipments/shipment_return_voucher_additional_service.rb +14 -0
  103. data/lib/speedy/models/shipments/shipment_rod_additional_service.rb +15 -0
  104. data/lib/speedy/models/shipments/shipment_rop_additional_service.rb +12 -0
  105. data/lib/speedy/models/shipments/shipment_rop_additional_service_line.rb +12 -0
  106. data/lib/speedy/models/shipments/shipment_sender.rb +23 -0
  107. data/lib/speedy/models/shipments/shipment_service.rb +17 -0
  108. data/lib/speedy/models/shipments/shipment_swap_additional_service.rb +17 -0
  109. data/lib/speedy/models/shipments/update_shipment_response.rb +10 -0
  110. data/lib/speedy/models/tracking/bulk_tracking_data_file.rb +14 -0
  111. data/lib/speedy/models/tracking/external_carrier_parcel_number_details.rb +13 -0
  112. data/lib/speedy/models/tracking/tracked_parcel.rb +21 -0
  113. data/lib/speedy/models/tracking/tracked_parcel_operation.rb +22 -0
  114. data/lib/speedy/models/tracking/tracked_parcel_operation_additional_info.rb +17 -0
  115. data/lib/speedy/models/tracking/tracked_parcel_operation_additional_info_predict.rb +14 -0
  116. data/lib/speedy/models/tracking/tracked_parcel_operation_additional_info_recipient.rb +14 -0
  117. data/lib/speedy/models/validation/validation_response.rb +14 -0
  118. data/lib/speedy/resource_attributes.rb +87 -0
  119. data/lib/speedy/service_resource.rb +94 -0
  120. data/lib/speedy/services/calculation.rb +22 -0
  121. data/lib/speedy/services/clients.rb +40 -0
  122. data/lib/speedy/services/courier_services.rb +25 -0
  123. data/lib/speedy/services/location/block.rb +32 -0
  124. data/lib/speedy/services/location/complex.rb +39 -0
  125. data/lib/speedy/services/location/country.rb +70 -0
  126. data/lib/speedy/services/location/office.rb +50 -0
  127. data/lib/speedy/services/location/point_of_interest.rb +39 -0
  128. data/lib/speedy/services/location/postcode.rb +25 -0
  129. data/lib/speedy/services/location/site.rb +77 -0
  130. data/lib/speedy/services/location/state.rb +46 -0
  131. data/lib/speedy/services/location/street.rb +39 -0
  132. data/lib/speedy/services/location.rb +11 -0
  133. data/lib/speedy/services/payments.rb +19 -0
  134. data/lib/speedy/services/pickup.rb +38 -0
  135. data/lib/speedy/services/print.rb +58 -0
  136. data/lib/speedy/services/shipments/shipment.rb +120 -0
  137. data/lib/speedy/services/shipments.rb +11 -0
  138. data/lib/speedy/services/tracking.rb +26 -0
  139. data/lib/speedy/services/validation/address.rb +26 -0
  140. data/lib/speedy/services/validation/general.rb +37 -0
  141. data/lib/speedy/services/validation.rb +11 -0
  142. data/lib/speedy/transport_error.rb +17 -0
  143. data/lib/speedy/util.rb +67 -0
  144. data/lib/speedy/version.rb +5 -0
  145. data/lib/speedy-bg.rb +6 -0
  146. data/lib/speedy.rb +203 -0
  147. data/sig/speedy.rbs +374 -0
  148. metadata +227 -0
@@ -0,0 +1,55 @@
1
+ # Offline response contracts
2
+
3
+ `spec/response_property_examples_spec.rb` exercises every endpoint shortcut in
4
+ `Speedy::ClientResources` through a real configured client and a local Faraday
5
+ test adapter. It makes no HTTP requests. A coverage assertion fails when a new
6
+ public shortcut lacks a scenario; the calculation alias is listed explicitly.
7
+
8
+ The checked-in `spec/fixtures/responses/*.json` files contain one synthetic
9
+ example per response type, assembled from the official schema snapshot. They exercise optional
10
+ properties as well as the required ones. They are examples of response shapes,
11
+ not successful live recordings or evidence that every optional property occurs
12
+ together. Tests never load the gitignored `schema/` directory. The authoritative
13
+ reference is the [Speedy API documentation](https://api.speedy.bg/api/docs/).
14
+
15
+ The recursive `expect_response_properties(actual, raw)` assertion derives a
16
+ snake_case accessor independently from each original JSON key. It calls the
17
+ accessor and compares every leaf with the original response value, including
18
+ false, zero, explicit null, nested models and every array item. The three
19
+ explicit dictionary structures (`details`, `detailsLocal`, and
20
+ `externalCarrierParcelNumbersDetails`) use hash-key lookup, not dot readers;
21
+ dictionaries retain the client's existing snake_case normalization
22
+ of keys, and typed dictionary values are checked using their model accessors.
23
+ Unknown scalar attributes are also checked. Regression examples prove the
24
+ helper rejects absent accessors, dropped nested fields, incorrect values and
25
+ raw hashes in place of typed nested models.
26
+
27
+ There are two explicit conversions rather than consulting the model's field
28
+ definitions to manufacture expected values: documented Date accessors use
29
+ `Date.parse`, and a null parcel array becomes `[]` under the existing array
30
+ casting contract. CSV cases independently list every expected column and its
31
+ typed value. Scalar client IDs and barcode lists are compared directly; empty
32
+ success objects have no leaf properties. PDF methods compare unchanged bytes,
33
+ including binary data. Error objects are checked through both a resource and a
34
+ collection response.
35
+
36
+ ## Schema limitations and discrepancies
37
+
38
+ - Extended print's schema describes `data` as an integer array, while the
39
+ documentation describes a base64 string. Its example follows the documented
40
+ JSON representation.
41
+ - External carrier parcel details are documented as a map, so populated maps
42
+ now return typed values instead of being treated as arrays. Empty arrays
43
+ seen in recordings remain readable for compatibility.
44
+ - Calculation additional services, courier-service allowances and office
45
+ maximum parcel dimensions now expose schema-backed nested model readers.
46
+ - The schema's unresolved `OfficeRoutingInformation` reference is covered by
47
+ the documentation's [RoutingInformation definition, section 3.34](https://api.speedy.bg/api/docs/#href-routing-information)
48
+ and [Tour definition, section 3.34.1](https://api.speedy.bg/api/docs/#href-routing-information-tour).
49
+ Office responses now expose typed routing and tour readers; all three office
50
+ JSON examples contain populated routing data checked recursively.
51
+ - `WorkDayType` and `DeliveryLimitViolationAutoAdjustment` have unresolved references in the
52
+ available schema snapshot. Their example values are null; no nested contract
53
+ is invented for these structures.
54
+ - These examples test parsing and accessor fidelity. Existing VCR/live specs
55
+ remain the evidence for actual server behavior and account restrictions.
data/docs/VCR.md ADDED
@@ -0,0 +1,161 @@
1
+ # VCR contract testing
2
+
3
+ The opt-in contract suite records real read-only API responses and replays them
4
+ later with external network access disabled. Each endpoint has an independent
5
+ cassette. Public nomenclature contracts run with stable Bulgarian defaults;
6
+ account and shipment reads require the fixture identifiers described in
7
+ `.env.example` and are structurally sanitized before being written.
8
+
9
+ CI runs the committed recordings separately from the opt-in live suite:
10
+
11
+ ```bash
12
+ SPEEDY_VCR_REPLAY=1 bundle exec rspec spec/recorded_contract_spec.rb
13
+ ```
14
+
15
+ Replay does not load `.env` files or require carrier credentials. It uses
16
+ `record: :none`, rejects unmatched requests, and blocks network access even if
17
+ a cassette's record mode is overridden. Missing cassettes cannot be recorded by CI.
18
+
19
+ PDF printing has read-only contracts in `spec/live/printing_contract_spec.rb`.
20
+ They use existing demo parcel/shipment IDs and never pass `printerName`, so no
21
+ physical printing is requested. Raw PDF bodies and base64 PDF data are replaced
22
+ entirely before recording; metadata is structurally sanitized. Replays verify
23
+ the response envelope, document signature and metadata types, not PDF contents
24
+ or visual correctness. The retained PDF signature is only kept when the actual
25
+ document starts with `%PDF-`; unexpected opaque bodies are replaced with a
26
+ non-PDF marker so a failed request cannot become a passing PDF fixture.
27
+
28
+ To record these contracts, set `SPEEDY_CONTRACT_PARCEL_IDS` and
29
+ `SPEEDY_CONTRACT_SHIPMENT_IDS` to existing, printable demo fixtures, then run:
30
+
31
+ ```bash
32
+ SPEEDY_LIVE_TESTS=1 bundle exec rspec spec/live/printing_contract_spec.rb
33
+ ```
34
+
35
+ ZPL output is not covered by these PDF-specific contracts.
36
+
37
+ Offline request-shape specs exercise every explicit public keyword at least
38
+ once. Hash payloads and `**options` pass through generic serialization, so their
39
+ documented representative fields and model casting are tested without trying
40
+ every possible field combination. Live contracts use the smallest meaningful
41
+ payload for each server behavior; they are not a parameter-permutation suite.
42
+
43
+ ## Coverage policy
44
+
45
+ | Endpoint kind | Contract approach |
46
+ | --- | --- |
47
+ | Public location and service nomenclatures | Recorded directly, one cassette per endpoint |
48
+ | Client, payout, calculation, shipment, tracking, and label-info reads | Recorded with dedicated demo fixtures and structural response redaction |
49
+ | Bulk CSV nomenclatures | Opt-in because Speedy may require account permission; one cassette per dataset |
50
+ | PDF labels, extended PDF labels, and PDF vouchers | Read-only fixtures; opaque document contents replaced entirely before recording |
51
+ | ZPL output | Offline coverage only; no live ZPL contract yet |
52
+ | Shipment, pickup, contact, and courier mutations | Excluded from the read-only suite; use the lifecycle pattern below |
53
+
54
+ ## Mutating endpoints
55
+
56
+ VCR only records HTTP; it does not roll back the carrier. Projects that test
57
+ creation or updates against a real sandbox usually record a complete scenario:
58
+
59
+ 1. create a uniquely identifiable shipment/contact/pickup in a dedicated demo
60
+ account;
61
+ 2. exercise the update, handover, or cancellation;
62
+ 3. query and assert the resulting server state;
63
+ 4. cancel or delete the fixture in an `ensure` cleanup block; and
64
+ 5. record and verify the cleanup response in the same cassette.
65
+
66
+ Recording is manual. CI only replays reviewed cassettes and must never have
67
+ network access or carrier credentials. If cleanup is unavailable or unreliable,
68
+ use a provider-reset sandbox, disposable pre-provisioned fixtures, or an offline
69
+ request-shape test instead of a live mutation.
70
+
71
+ Before committing a recording, verify cleanup in the carrier UI, inspect the
72
+ diff, scan for credentials and personal data, and retain JSON types when
73
+ replacing identifiers. Never bulk re-record the entire suite automatically;
74
+ delete and refresh one cassette at a time.
75
+
76
+ ## Reviewed mutation example
77
+
78
+ `spec/live/shipment_mutation_contract_spec.rb` implements the lifecycle above
79
+ for shipment creation. It creates a synthetic office-delivery shipment and
80
+ cancels it in an `ensure` block, then calls `shipment_info` to document that
81
+ Speedy retains the cancelled shipment as a historical record. It then tracks
82
+ the created parcel with its full operation history. The demo recording returns
83
+ an empty operation list immediately after cancellation, which the example
84
+ asserts. All four interactions are stored in
85
+ `speedy_api/shipment_create_cancel_and_verify.yml`; the generated shipment ID
86
+ is filtered.
87
+
88
+ The example requires an additional, deliberate opt-in:
89
+
90
+ ```bash
91
+ SPEEDY_LIVE_TESTS=1 SPEEDY_MUTATING_LIVE_TESTS=1 \
92
+ bundle exec rspec spec/live/shipment_mutation_contract_spec.rb
93
+ ```
94
+
95
+ Before recording, verify that `SPEEDY_CONTRACT_OFFICE_ID` accepts service 505
96
+ and that the configured sender client may create shipments. A successful
97
+ `shipment/cancel` response is the authoritative cleanup confirmation. The
98
+ documented `shipment/info` response has no cancellation-status field, so the
99
+ final lookup verifies record retention and the generated identifier but cannot
100
+ independently prove cancellation. Tracking operation `128` means Canceled, but
101
+ the recorded uncollected shipment has no tracking operations after cancellation.
102
+ Neither an empty tracking history nor a retained shipment record independently
103
+ proves cancellation. This lifecycle relies on the documented successful cancel
104
+ response for cleanup confirmation. There is no documented dedicated cancellation
105
+ status field in the shipment information response.
106
+
107
+ ## Mutation cleanup inventory
108
+
109
+ The classifications below describe the published API, not a guarantee that a
110
+ demo account accepts every payload. Cancellation is compensating cleanup: it
111
+ leaves a historical record and does not roll back all stored data.
112
+ Sources: [shipment, pickup, and contact API documentation](https://api.speedy.bg/api/docs/).
113
+
114
+ ### Clear documented cleanup paths
115
+
116
+ | Endpoint | Scenario and cleanup | Recording status |
117
+ | --- | --- | --- |
118
+ | Create and cancel shipment | Create an uncollected shipment without requesting pickup; cancel it in `ensure` | Existing demo cassette confirms successful cancellation |
119
+ | Update shipment | Create a disposable shipment, replace its contents, query the changed contents, cancel | Recorded as `shipment_update_and_cancel` |
120
+ | Update shipment properties | Create a disposable shipment, change the `contents` property, query it, cancel | Recorded as `shipment_properties_and_cancel` |
121
+
122
+ All scenarios are in `spec/live/shipment_mutation_contract_spec.rb` and use
123
+ separate cassettes. Cleanup runs even if an operation or assertion fails after
124
+ creation returns an identifier. If creation times out before returning an ID,
125
+ automatic cleanup cannot identify the remote fixture; reconcile the account
126
+ before retrying. Cleanup failures fail the example and require manual review.
127
+ No pickup or handover is performed.
128
+
129
+ Existing cassettes replay as usual; do not delete them without planning another
130
+ disposable fixture. Read-back assertions check contents before cleanup. Successful
131
+ `shipment/cancel` remains the cancellation confirmation, not tracking history.
132
+
133
+ ### Unavailable in the standard demo account
134
+
135
+ Adding parcels and finalizing a pending shipment require permission to create
136
+ with `pendingParcels`. The standard test account does not have this permission,
137
+ so these endpoints have offline request/model coverage only. The live lifecycle
138
+ and its capability flag were removed. Cancelling the parent shipment is the
139
+ documented cleanup approach for an authorized account, but that path has not
140
+ been verified in the demo environment.
141
+
142
+ ### Suggested cleanup with uncertain effectiveness
143
+
144
+ | Endpoint | Possible approach | Uncertainty |
145
+ | --- | --- | --- |
146
+ | Request pickup | Cancel every associated disposable shipment, then ask the carrier to cancel/reset the pickup order | Cancelling shipments is not documented to cancel the courier visit or pickup order; no pickup cancellation endpoint is exposed |
147
+
148
+ This is a suggestion only and has no live mutation spec. Confirm order-level
149
+ cleanup with the carrier before recording it.
150
+
151
+ ### No obvious API cleanup path
152
+
153
+ | Endpoint/action | Missing inverse |
154
+ | --- | --- |
155
+ | Create contact | No contact-delete endpoint; overwriting a contact would not remove it and could alter an existing account record |
156
+ | Handover to courier | No undo-handover endpoint; do not assume shipment cancellation reverses a registered handover |
157
+ | Handover to midway carrier | No undo for the registered external handover |
158
+ | Direct printing using `printerName` | Physical printing cannot be undone through the API; read-only print contracts omit this option |
159
+
160
+ These need carrier-managed reset or explicitly disposable fixtures. They remain
161
+ covered offline rather than by automatic live mutation scenarios.
@@ -0,0 +1,38 @@
1
+ # Speedy schema fidelity notes
2
+
3
+ The implementation is based on the JSON schemas distributed with Speedy's API
4
+ documentation. Those files describe payload shapes, but a schema file alone is
5
+ not evidence that a public HTTP route exists or remains enabled for every
6
+ account.
7
+
8
+ ## Address search schemas without a route
9
+
10
+ The schema bundle contains `SearchAddressRequest`, `SearchAddressResponse`, and
11
+ `SearchAddress`. The request describes site, complex, street,
12
+ point-of-interest, and building-number inputs, while the response contains
13
+ address suggestions. However, the provided endpoint documentation does not
14
+ associate those types with an HTTP path.
15
+
16
+ No route is inferred from the Java package identifier or type name because an
17
+ incorrect route could create an apparently supported API that fails against
18
+ real accounts. Consequently, `Speedy::Client` does not expose address search.
19
+ It can be added compatibly when Speedy publishes an authoritative route and
20
+ request contract.
21
+
22
+ ## Representation decisions
23
+
24
+ - JSON object responses with documented fields use typed `Speedy::Model`
25
+ subclasses.
26
+ - Batch responses use `Speedy::Collection`, preserving top-level API errors.
27
+ - PDF and ZPL endpoints return binary strings; the gem does not decode or write
28
+ them to disk.
29
+ - CSV nomenclature exports are parsed into the same model classes as JSON
30
+ nomenclature results.
31
+ - Fields whose nested shape is not defined authoritatively remain arrays,
32
+ hashes, or untyped model attributes rather than speculative classes.
33
+ - Undocumented fields are preserved in `Model#to_h`, so additive carrier
34
+ changes do not discard information.
35
+
36
+ These notes describe documentation-derived support. They are not a substitute
37
+ for testing with the Speedy environment and permissions assigned to a specific
38
+ contract.
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+
5
+ module Speedy
6
+ class CamelCaseMiddleware < Faraday::Middleware
7
+ DEFAULT_ACRONYMS = {
8
+ 'id' => 'Id',
9
+ 'ids' => 'Ids',
10
+ 'cod' => 'COD',
11
+ 'pudo' => 'PUDO'
12
+ }.freeze
13
+
14
+ def initialize(app, acronyms: {}, replace_defaults: false)
15
+ super(app)
16
+ @acronyms =
17
+ if acronyms.nil?
18
+ {}
19
+ elsif replace_defaults
20
+ acronyms.transform_keys(&:to_s)
21
+ else
22
+ DEFAULT_ACRONYMS.merge(acronyms.transform_keys(&:to_s))
23
+ end
24
+ @acronym_reverse = @acronyms.each_with_object({}) { |(key, value), hash| hash[value] = key }
25
+ end
26
+
27
+ def call(env)
28
+ env.body = deep_transform_keys(env.body) { |key| to_camel_case(key) } if env.body.is_a?(Hash)
29
+ @app.call(env)
30
+ end
31
+
32
+ def on_complete(env)
33
+ env.body = deep_transform_keys(env.body) { |key| to_snake_case(key) } if env.body.is_a?(Hash)
34
+ end
35
+
36
+ private
37
+
38
+ def to_camel_case(key)
39
+ parts = key.to_s.split('_')
40
+ first = parts.shift
41
+
42
+ first + parts.map { |part| @acronyms.fetch(part, part.capitalize) }.join
43
+ end
44
+
45
+ def to_snake_case(key)
46
+ str = key.to_s
47
+ @acronym_reverse.keys.sort_by { |acro| -acro.length }.each do |acro|
48
+ str = str.gsub(/#{acro}(?=[A-Z]|$)/) { "_#{@acronym_reverse[acro]}_" }
49
+ end
50
+
51
+ str
52
+ .gsub(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
53
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
54
+ .downcase
55
+ .gsub(/__+/, '_')
56
+ .sub(/^_/, '')
57
+ .sub(/_$/, '')
58
+ end
59
+
60
+ def deep_transform_keys(obj, &block)
61
+ case obj
62
+ when Array
63
+ obj.map { |item| deep_transform_keys(item, &block) }
64
+ when Hash
65
+ obj.each_with_object({}) { |(key, value), hash| hash[yield(key)] = deep_transform_keys(value, &block) }
66
+ else
67
+ obj
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'json'
5
+ require 'logger'
6
+
7
+ module Speedy
8
+ class Client
9
+ include Speedy::ClientResources
10
+
11
+ class << self
12
+ attr_reader :instance
13
+
14
+ def configure
15
+ @instance ||= new
16
+ yield(@instance) if block_given?
17
+ @instance.setup_connection!
18
+ end
19
+
20
+ def connection
21
+ instance&.connection || raise('Speedy::Client is not configured. Call Speedy::Client.configure first.')
22
+ end
23
+ end
24
+
25
+ attr_accessor :username, :password, :base_url, :adapter,
26
+ :language, :client_system_id,
27
+ :acronyms, :replace_acronym_defaults,
28
+ :debug, :debug_bodies, :debug_logger,
29
+ :open_timeout, :read_timeout, :write_timeout
30
+
31
+ def initialize
32
+ @base_url = 'https://api.speedy.bg/v1/'
33
+ @adapter = Faraday.default_adapter
34
+ @language = nil
35
+ @client_system_id = nil
36
+ @acronyms = {}
37
+ @replace_acronym_defaults = false
38
+ @debug = false
39
+ @debug_bodies = false
40
+ @debug_logger = Logger.new($stdout)
41
+ @open_timeout = 30
42
+ @read_timeout = 30
43
+ @write_timeout = 30
44
+ end
45
+
46
+ def add_acronyms(hash)
47
+ @acronyms = (@acronyms || {}).merge(hash.transform_keys(&:to_s))
48
+ end
49
+
50
+ def setup_connection!
51
+ validate_credentials!
52
+
53
+ @connection = Faraday.new(base_url) do |conn|
54
+ configure_timeouts(conn)
55
+ conn.use Speedy::CamelCaseMiddleware,
56
+ acronyms: @acronyms,
57
+ replace_defaults: @replace_acronym_defaults
58
+ conn.request :json
59
+ conn.response :json, content_type: 'application/json'
60
+ configure_debug_logging(conn) if debug
61
+ conn.adapter(*Array(adapter))
62
+ conn.headers['User-Agent'] = "SpeedyRubyClient/#{Speedy::VERSION}"
63
+ end
64
+ end
65
+
66
+ def connection
67
+ @connection || raise('Connection is not initialized. Call Speedy::Client.configure first.')
68
+ end
69
+
70
+ def credentials
71
+ {
72
+ user_name: username,
73
+ password: password,
74
+ language: language,
75
+ client_system_id: client_system_id
76
+ }.compact
77
+ end
78
+
79
+ private
80
+
81
+ def configure_debug_logging(connection)
82
+ connection.response :logger, debug_logger, bodies: debug_bodies do |formatter|
83
+ [username, password, client_system_id].each do |value|
84
+ formatter.filter(value.to_s, '[FILTERED]') unless value.to_s.empty?
85
+ end
86
+ end
87
+ end
88
+
89
+ def configure_timeouts(connection)
90
+ connection.options.open_timeout = open_timeout
91
+ connection.options.timeout = read_timeout
92
+ connection.options.write_timeout = write_timeout if connection.options.respond_to?(:write_timeout=)
93
+ end
94
+
95
+ def validate_credentials!
96
+ creds_missing = [username, password].any? { |credential| credential.to_s.empty? }
97
+ raise 'Username and password must be set' if creds_missing
98
+ end
99
+ end
100
+ end