kintsugi_sdk 5.5.12 → 5.6.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 (172) hide show
  1. checksums.yaml +4 -4
  2. data/lib/crystalline/enum.rbi +17 -0
  3. data/lib/crystalline/metadata_fields.rb +94 -38
  4. data/lib/crystalline/module.rb +103 -13
  5. data/lib/crystalline/types.rb +100 -16
  6. data/lib/crystalline/utils.rb +13 -0
  7. data/lib/kintsugi_sdk/addressvalidation.rb +3 -3
  8. data/lib/kintsugi_sdk/customers.rb +10 -10
  9. data/lib/kintsugi_sdk/exemptions.rb +7 -7
  10. data/lib/kintsugi_sdk/models/errors/backend_src_address_validation_responses_validationerrorresponse.rb +0 -1
  11. data/lib/kintsugi_sdk/models/errors/backend_src_customers_responses_validationerrorresponse.rb +0 -1
  12. data/lib/kintsugi_sdk/models/errors/backend_src_exemptions_responses_validationerrorresponse.rb +0 -1
  13. data/lib/kintsugi_sdk/models/errors/backend_src_products_responses_validationerrorresponse.rb +0 -1
  14. data/lib/kintsugi_sdk/models/errors/backend_src_tax_estimation_responses_validationerrorresponse.rb +0 -1
  15. data/lib/kintsugi_sdk/models/errors/backend_src_transactions_responses_validationerrorresponse.rb +0 -1
  16. data/lib/kintsugi_sdk/models/errors/errorresponse.rb +0 -1
  17. data/lib/kintsugi_sdk/models/errors/httpvalidationerror.rb +0 -1
  18. data/lib/kintsugi_sdk/models/ops/createtransactionbycustomer_request.rb +0 -1
  19. data/lib/kintsugi_sdk/models/ops/estimate_tax_v1_tax_estimate_post_request.rb +2 -3
  20. data/lib/kintsugi_sdk/models/ops/get_customer_by_external_id_v1_customers_external_external_id_get_request.rb +0 -1
  21. data/lib/kintsugi_sdk/models/ops/get_customer_by_id_v1_customers_customer_id_get_request.rb +0 -1
  22. data/lib/kintsugi_sdk/models/ops/get_customers_v1_request.rb +0 -1
  23. data/lib/kintsugi_sdk/models/ops/get_exemption_by_id_v1_exemptions_exemption_id_get_request.rb +0 -1
  24. data/lib/kintsugi_sdk/models/ops/get_exemptions_v1_exemptions_get_request.rb +0 -1
  25. data/lib/kintsugi_sdk/models/ops/get_nexus_for_org_v1_nexus_get_request.rb +12 -5
  26. data/lib/kintsugi_sdk/models/ops/get_nexus_for_org_v1_nexus_get_request.rbi +4 -0
  27. data/lib/kintsugi_sdk/models/ops/get_product_by_id_v1_products_product_id_get_request.rb +0 -1
  28. data/lib/kintsugi_sdk/models/ops/get_transaction_by_id_v1_transactions_transaction_id_get_request.rb +0 -1
  29. data/lib/kintsugi_sdk/models/ops/get_transactions_v1_transactions_get_request.rb +0 -1
  30. data/lib/kintsugi_sdk/models/ops/getexemptionattachments_request.rb +0 -1
  31. data/lib/kintsugi_sdk/models/ops/gettransactionbyexternalid_request.rb +0 -1
  32. data/lib/kintsugi_sdk/models/ops/gettransactionsbycustomer_request.rb +0 -1
  33. data/lib/kintsugi_sdk/models/ops/gettransactionsbyfiling_request.rb +0 -1
  34. data/lib/kintsugi_sdk/models/ops/search_v1_address_validation_search_post_security.rb +0 -1
  35. data/lib/kintsugi_sdk/models/ops/update_customer_v1_customers_customer_id_put_request.rb +0 -1
  36. data/lib/kintsugi_sdk/models/ops/update_product_v1_products_product_id_put_request.rb +0 -1
  37. data/lib/kintsugi_sdk/models/ops/update_transaction_v1_transactions_transaction_id_put_request.rb +0 -1
  38. data/lib/kintsugi_sdk/models/ops/uploadexemptioncert_request.rb +0 -1
  39. data/lib/kintsugi_sdk/models/ops.rb +0 -1
  40. data/lib/kintsugi_sdk/models/shared/addressbase.rb +1 -2
  41. data/lib/kintsugi_sdk/models/shared/addresses.rb +3 -4
  42. data/lib/kintsugi_sdk/models/shared/addressresponsedata.rb +0 -1
  43. data/lib/kintsugi_sdk/models/shared/addresssearchresponse.rb +0 -1
  44. data/lib/kintsugi_sdk/models/shared/addressstatus.rb +0 -3
  45. data/lib/kintsugi_sdk/models/shared/addresssubmittedresponse.rb +0 -1
  46. data/lib/kintsugi_sdk/models/shared/addresstype.rb +0 -3
  47. data/lib/kintsugi_sdk/models/shared/appliedto.rb +0 -3
  48. data/lib/kintsugi_sdk/models/shared/attachmentread.rb +1 -2
  49. data/lib/kintsugi_sdk/models/shared/backend_src_address_validation_responses_validationerroritem.rb +0 -1
  50. data/lib/kintsugi_sdk/models/shared/backend_src_customers_responses_validationerroritem.rb +0 -1
  51. data/lib/kintsugi_sdk/models/shared/backend_src_exemptions_models_exemptionread.rb +4 -5
  52. data/lib/kintsugi_sdk/models/shared/backend_src_exemptions_responses_validationerroritem.rb +0 -1
  53. data/lib/kintsugi_sdk/models/shared/backend_src_exemptions_serializers_exemptionread.rb +4 -5
  54. data/lib/kintsugi_sdk/models/shared/backend_src_products_responses_validationerroritem.rb +0 -1
  55. data/lib/kintsugi_sdk/models/shared/backend_src_tax_estimation_responses_validationerroritem.rb +0 -1
  56. data/lib/kintsugi_sdk/models/shared/backend_src_transactions_responses_validationerroritem.rb +0 -1
  57. data/lib/kintsugi_sdk/models/shared/body_upload_exemption_certificate_v1_exemptions_exemption_id_attachments_post.rb +0 -1
  58. data/lib/kintsugi_sdk/models/shared/countrycodeenum.rb +1 -3
  59. data/lib/kintsugi_sdk/models/shared/currencyenum.rb +0 -3
  60. data/lib/kintsugi_sdk/models/shared/customerbase.rb +4 -5
  61. data/lib/kintsugi_sdk/models/shared/customerbasebase.rb +4 -5
  62. data/lib/kintsugi_sdk/models/shared/customerbasepublic.rb +4 -5
  63. data/lib/kintsugi_sdk/models/shared/customercreate.rb +14 -7
  64. data/lib/kintsugi_sdk/models/shared/customercreate.rbi +4 -0
  65. data/lib/kintsugi_sdk/models/shared/customerread.rb +14 -7
  66. data/lib/kintsugi_sdk/models/shared/customerread.rbi +4 -0
  67. data/lib/kintsugi_sdk/models/shared/customertaxregistrationread.rb +52 -0
  68. data/lib/kintsugi_sdk/models/shared/customertaxregistrationread.rbi +23 -0
  69. data/lib/kintsugi_sdk/models/shared/customertaxtypeenum.rb +25 -0
  70. data/lib/kintsugi_sdk/models/shared/customertaxtypeenum.rbi +11 -0
  71. data/lib/kintsugi_sdk/models/shared/customerupdate.rb +10 -7
  72. data/lib/kintsugi_sdk/models/shared/customerupdate.rbi +2 -0
  73. data/lib/kintsugi_sdk/models/shared/discountbuilder.rb +2 -3
  74. data/lib/kintsugi_sdk/models/shared/exemption.rb +5 -6
  75. data/lib/kintsugi_sdk/models/shared/exemptioncreate.rb +4 -5
  76. data/lib/kintsugi_sdk/models/shared/exemptionrequired.rb +6 -5
  77. data/lib/kintsugi_sdk/models/shared/exemptionstatus.rb +0 -3
  78. data/lib/kintsugi_sdk/models/shared/exemptiontype.rb +0 -3
  79. data/lib/kintsugi_sdk/models/shared/fastapi_pagination_default_page_exemptionread_2.rb +3 -4
  80. data/lib/kintsugi_sdk/models/shared/file.rb +0 -1
  81. data/lib/kintsugi_sdk/models/shared/findthresholdcrossingtransactionstate.rb +56 -0
  82. data/lib/kintsugi_sdk/models/shared/findthresholdcrossingtransactionstate.rbi +25 -0
  83. data/lib/kintsugi_sdk/models/shared/jurisdictiontype.rb +0 -3
  84. data/lib/kintsugi_sdk/models/shared/nexusresponse.rb +41 -26
  85. data/lib/kintsugi_sdk/models/shared/nexusresponse.rbi +10 -2
  86. data/lib/kintsugi_sdk/models/shared/nexusstateenum.rb +0 -3
  87. data/lib/kintsugi_sdk/models/shared/nexusstatusenum.rb +0 -3
  88. data/lib/kintsugi_sdk/models/shared/nexustypeenum.rb +0 -3
  89. data/lib/kintsugi_sdk/models/shared/page_customerread_.rb +3 -4
  90. data/lib/kintsugi_sdk/models/shared/page_nexusresponse_.rb +3 -4
  91. data/lib/kintsugi_sdk/models/shared/page_transactionestimateresponse_.rb +3 -4
  92. data/lib/kintsugi_sdk/models/shared/page_transactionread_.rb +3 -4
  93. data/lib/kintsugi_sdk/models/shared/periodmodelenum.rb +0 -3
  94. data/lib/kintsugi_sdk/models/shared/processingstatusenum.rb +1 -3
  95. data/lib/kintsugi_sdk/models/shared/productread.rb +12 -9
  96. data/lib/kintsugi_sdk/models/shared/productstatusenum.rb +0 -3
  97. data/lib/kintsugi_sdk/models/shared/productupdate.rb +10 -7
  98. data/lib/kintsugi_sdk/models/shared/registration.rb +0 -1
  99. data/lib/kintsugi_sdk/models/shared/registrationsregimeenum.rb +0 -3
  100. data/lib/kintsugi_sdk/models/shared/relatedentitytype.rb +0 -3
  101. data/lib/kintsugi_sdk/models/shared/salesortransactionsenum.rb +0 -3
  102. data/lib/kintsugi_sdk/models/shared/security.rb +0 -1
  103. data/lib/kintsugi_sdk/models/shared/sourceenum.rb +10 -4
  104. data/lib/kintsugi_sdk/models/shared/statusenum.rb +0 -3
  105. data/lib/kintsugi_sdk/models/shared/taxexemptionenum.rb +1 -3
  106. data/lib/kintsugi_sdk/models/shared/taxitembuilder.rb +4 -5
  107. data/lib/kintsugi_sdk/models/shared/taxitemestimate.rb +1 -2
  108. data/lib/kintsugi_sdk/models/shared/taxitemread.rb +4 -5
  109. data/lib/kintsugi_sdk/models/shared/taxitemreturnreasonenum.rb +2 -3
  110. data/lib/kintsugi_sdk/models/shared/taxitemtypeenum.rb +0 -3
  111. data/lib/kintsugi_sdk/models/shared/taxliabilitysourceenum.rb +0 -3
  112. data/lib/kintsugi_sdk/models/shared/transactionaddressbuilder.rb +3 -4
  113. data/lib/kintsugi_sdk/models/shared/transactionaddresspublic.rb +2 -3
  114. data/lib/kintsugi_sdk/models/shared/transactionaddressread_output.rb +3 -4
  115. data/lib/kintsugi_sdk/models/shared/transactioncreate.rb +23 -19
  116. data/lib/kintsugi_sdk/models/shared/transactioncreate.rbi +2 -2
  117. data/lib/kintsugi_sdk/models/shared/transactionestimatepublicrequest.rb +6 -11
  118. data/lib/kintsugi_sdk/models/shared/transactionestimatepublicrequest.rbi +0 -2
  119. data/lib/kintsugi_sdk/models/shared/transactionestimateresponse.rb +7 -12
  120. data/lib/kintsugi_sdk/models/shared/transactionestimateresponse.rbi +0 -2
  121. data/lib/kintsugi_sdk/models/shared/transactionestimateresponse_addresses.rb +3 -4
  122. data/lib/kintsugi_sdk/models/shared/transactionestimateresponse_type.rb +0 -3
  123. data/lib/kintsugi_sdk/models/shared/transactionexemptstatusenum.rb +2 -3
  124. data/lib/kintsugi_sdk/models/shared/transactionitembuilder.rb +8 -7
  125. data/lib/kintsugi_sdk/models/shared/transactionitemcreateupdate.rb +8 -7
  126. data/lib/kintsugi_sdk/models/shared/transactionitemestimatebase.rb +9 -8
  127. data/lib/kintsugi_sdk/models/shared/transactionitemestimateresponse.rb +10 -9
  128. data/lib/kintsugi_sdk/models/shared/transactionitemread.rb +7 -6
  129. data/lib/kintsugi_sdk/models/shared/transactionpublicrequest.rb +21 -17
  130. data/lib/kintsugi_sdk/models/shared/transactionpublicrequest.rbi +2 -2
  131. data/lib/kintsugi_sdk/models/shared/transactionread.rb +24 -20
  132. data/lib/kintsugi_sdk/models/shared/transactionread.rbi +2 -2
  133. data/lib/kintsugi_sdk/models/shared/transactionrefundstatus.rb +0 -3
  134. data/lib/kintsugi_sdk/models/shared/transactionstatusenum.rb +0 -3
  135. data/lib/kintsugi_sdk/models/shared/transactiontypeenum.rb +0 -3
  136. data/lib/kintsugi_sdk/models/shared/transactionupdate.rb +19 -60
  137. data/lib/kintsugi_sdk/models/shared/transactionupdate.rbi +2 -24
  138. data/lib/kintsugi_sdk/models/shared/treatmentenum.rb +0 -3
  139. data/lib/kintsugi_sdk/models/shared/type.rb +0 -3
  140. data/lib/kintsugi_sdk/models/shared/validationaddress.rb +0 -1
  141. data/lib/kintsugi_sdk/models/shared/validationerror.rb +0 -1
  142. data/lib/kintsugi_sdk/models/shared.rb +3 -7
  143. data/lib/kintsugi_sdk/nexus.rb +1 -1
  144. data/lib/kintsugi_sdk/openapisdk.rb +13 -15
  145. data/lib/kintsugi_sdk/products.rb +6 -414
  146. data/lib/kintsugi_sdk/sdk_hooks/hooks.rb +0 -1
  147. data/lib/kintsugi_sdk/sdkconfiguration.rb +5 -10
  148. data/lib/kintsugi_sdk/taxestimation.rb +4 -4
  149. data/lib/kintsugi_sdk/transactions.rb +8 -8
  150. data/lib/kintsugi_sdk/utils/forms.rb +1 -1
  151. data/lib/kintsugi_sdk/utils/query_params.rb +4 -2
  152. data/lib/kintsugi_sdk/utils/request_bodies.rb +11 -6
  153. data/lib/kintsugi_sdk/utils/security.rb +10 -0
  154. data/lib/kintsugi_sdk/utils/url.rb +11 -5
  155. data/lib/kintsugi_sdk/utils/utils.rb +45 -3
  156. metadata +77 -41
  157. data/lib/kintsugi_sdk/models/ops/get_products_v1_products_get_request.rb +0 -61
  158. data/lib/kintsugi_sdk/models/ops/get_products_v1_products_get_request.rbi +0 -27
  159. data/lib/kintsugi_sdk/models/shared/page_productread_.rb +0 -49
  160. data/lib/kintsugi_sdk/models/shared/page_productread_.rbi +0 -21
  161. data/lib/kintsugi_sdk/models/shared/productcategories.rb +0 -38
  162. data/lib/kintsugi_sdk/models/shared/productcategories.rbi +0 -15
  163. data/lib/kintsugi_sdk/models/shared/productcategoryenum.rb +0 -25
  164. data/lib/kintsugi_sdk/models/shared/productcategoryenum.rbi +0 -11
  165. data/lib/kintsugi_sdk/models/shared/productcodeenum.rb +0 -82
  166. data/lib/kintsugi_sdk/models/shared/productcodeenum.rbi +0 -11
  167. data/lib/kintsugi_sdk/models/shared/productcreatemanual.rb +0 -61
  168. data/lib/kintsugi_sdk/models/shared/productcreatemanual.rbi +0 -27
  169. data/lib/kintsugi_sdk/models/shared/productsubcategory.rb +0 -46
  170. data/lib/kintsugi_sdk/models/shared/productsubcategory.rbi +0 -19
  171. data/lib/kintsugi_sdk/models/shared/productsubcategoryenum.rb +0 -83
  172. data/lib/kintsugi_sdk/models/shared/productsubcategoryenum.rbi +0 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b66214fde827ac38fd31fee61c7df59df002febd0d00d3751c7e091a08f3d5a
4
- data.tar.gz: 2e13c5795d9a2d25e8e5b84a79dfc6e7b6fa1ec0689460f501180a7ad3b05e1a
3
+ metadata.gz: 1c784880b4a422c7a0e66e7daade32a4b3124070d07890bb142130130af70a2a
4
+ data.tar.gz: 96c58cc09e56bbfeececd7e9db075e6143a7eeb9804d88e63c5e361d5a00e338
5
5
  SHA512:
6
- metadata.gz: 84c605ff4220af8891389f27bbe01669973d2677502f39905ae26d3e59343b2d608757601489f5c6c9e3887e37c6b4f496819df4c2403cd1c84e3b99f6748ee9
7
- data.tar.gz: 96fd5833f0ee4e3aac46fb83995a1cddc7a789f67e9436948dd857e8268ff218fc86564f71792e5ac3e6837bba8d6525967f75d2fa6982c09ca71730094a5b32
6
+ metadata.gz: 6379d79bb6881467f0ae4e9ff58dd723f8d29c324d2c0becc133ee175bff87c9858ab5bf745a5cdaca985182b98fc7a32b9e7f6d44fba7b5846e6c713fa74c39
7
+ data.tar.gz: 4fb9b28a9297f3b043286ee5a1e8f078c698e6ac406d876fdb1e5fd8f3e0e2d3a71f1513f21b9ac4205496c840f6677f57ab8ba109dbed05a8c3746d6dcb5423
@@ -0,0 +1,17 @@
1
+ # typed: true
2
+
3
+ module Crystalline
4
+ module Enum
5
+ mixes_in_class_methods(ClassMethods)
6
+
7
+ module ClassMethods
8
+ def enums(&blk); end
9
+ def open!; end
10
+ def open?; end
11
+ def deserialize(val); end
12
+ end
13
+
14
+ def serialize; end
15
+ def known?; end
16
+ end
17
+ end
@@ -37,19 +37,30 @@ module Crystalline
37
37
  def unmarshal_single(field_type, value, format_metadata = nil)
38
38
  decoder = format_metadata.fetch(:decoder, nil)
39
39
 
40
- if field_type.instance_of?(Class) && field_type.include?(::Crystalline::MetadataFields)
40
+ # Delegate complex Crystalline types to unmarshal_json
41
+ if field_type.is_a?(Crystalline::DiscriminatedUnion) ||
42
+ Crystalline::Utils.arr?(field_type) ||
43
+ Crystalline::Utils.hash?(field_type) ||
44
+ Crystalline::Utils.union?(field_type)
45
+ return Crystalline.unmarshal_json(value, field_type)
46
+ elsif field_type.instance_of?(Class) && field_type.include?(::Crystalline::MetadataFields)
41
47
  return field_type.from_dict(value)
42
48
  elsif field_type.to_s == 'Date'
43
49
  return Date.parse(value)
44
50
  elsif field_type.to_s == 'DateTime'
45
51
  return DateTime.parse(value)
46
52
  elsif field_type.to_s == 'Object'
47
- # rubocop:disable Lint/SuppressedException
48
- begin
49
- value = JSON.parse(value)
50
- rescue TypeError, JSON::ParserError
53
+ if value.is_a?(::String)
54
+ trimmed = value.lstrip
55
+ if trimmed.start_with?('{') || trimmed.start_with?('[')
56
+ # rubocop:disable Lint/SuppressedException
57
+ begin
58
+ value = JSON.parse(value)
59
+ rescue TypeError, JSON::ParserError
60
+ end
61
+ # rubocop:enable Lint/SuppressedException
62
+ end
51
63
  end
52
- # rubocop:enable Lint/SuppressedException
53
64
  return value
54
65
  elsif field_type.to_s == 'Float'
55
66
  return value.to_f
@@ -62,29 +73,30 @@ module Crystalline
62
73
  end
63
74
  end
64
75
 
65
- def from_json(json_obj)
66
- case json_obj
67
- when String
68
- begin
69
- d = JSON.parse(json_obj)
70
- rescue JSON::ParserError
71
- d = json_obj
72
- end
73
- else
74
- d = json_obj
75
- end
76
- from_json(d)
77
- end
78
-
79
76
  def from_dict(d)
80
77
  to_build = {}
81
78
 
79
+ # Collect lookup keys for regular fields so we can identify additional properties later
80
+ known_keys = {}
81
+ additional_props_fields = []
82
82
  fields.each do |field|
83
+ format_metadata = field.metadata.fetch(:format_json, {})
84
+ if format_metadata.fetch(:additional_properties, false)
85
+ additional_props_fields << field
86
+ else
87
+ lookup = format_metadata.fetch(:letter_case, nil).call
88
+ known_keys[lookup] = true
89
+ end
90
+ end
91
+
92
+ # Process regular fields
93
+ (fields - additional_props_fields).each do |field|
83
94
  key = field.name
84
95
  format_metadata = field.metadata.fetch(:format_json, {})
96
+ field_type = field.type
97
+
85
98
  lookup = format_metadata.fetch(:letter_case, nil).call
86
99
  value = d[lookup]
87
- field_type = field.type
88
100
  if ::Crystalline::Utils.nilable? field_type
89
101
  if value == 'null'
90
102
  to_build[key] = nil
@@ -92,13 +104,15 @@ module Crystalline
92
104
  end
93
105
  field_type = ::Crystalline::Utils.nilable_of(field_type)
94
106
  end
95
-
107
+
96
108
  # If field is not nilable, and the value is not in the dict, raise a KeyError
97
109
  raise KeyError, "key #{lookup} not found in hash" if value.nil? && !::Crystalline::Utils.nilable?(field.type)
98
110
  # If field is nilable, and the value is not in the dict, just move to the next field
99
111
  next if value.nil?
100
112
 
101
- if Crystalline::Utils.arr? field_type
113
+ if field_type.is_a?(Crystalline::DiscriminatedUnion)
114
+ to_build[key] = field_type.parse(value)
115
+ elsif Crystalline::Utils.arr? field_type
102
116
  inner_type = Crystalline::Utils.arr_of(field_type)
103
117
  unmarshalled_array = value.map { |f| unmarshal_single(inner_type, f, format_metadata) }
104
118
  to_build[key] = unmarshalled_array
@@ -111,25 +125,36 @@ module Crystalline
111
125
  to_build[key] = unmarshalled_hash
112
126
  elsif Crystalline::Utils.union? field_type
113
127
  discriminator = field.metadata.fetch(:discriminator, nil)
128
+ discriminator_mapping = field.metadata.fetch(:discriminator_mapping, nil)
114
129
  if !discriminator.nil?
115
- type_to_deserialize = value.fetch(discriminator)
116
- type_to_deserialize = Crystalline::Utils.get_union_types(field_type).find { |t| t.name.split('::').last == type_to_deserialize }
130
+ discriminator_value = value.fetch(discriminator)
131
+ if !discriminator_mapping.nil?
132
+ # Use explicit mapping from discriminator value to type
133
+ type_to_deserialize = discriminator_mapping[discriminator_value]
134
+ else
135
+ # Fallback: try to match discriminator value against type name
136
+ type_to_deserialize = Crystalline::Utils.get_union_types(field_type).find { |t| t.name.split('::').last == discriminator_value }
137
+ end
117
138
  to_build[key] = Crystalline.unmarshal_json(value, type_to_deserialize)
118
139
  else
119
140
  union_types = Crystalline::Utils.get_union_types(field_type)
120
141
  union_types = union_types.sort_by { |klass| Crystalline.non_nilable_attr_count(klass) }
121
142
 
122
- union_types.each do |union_type|
123
- begin
124
- to_build[key] = Crystalline.unmarshal_json(value, union_type)
125
- rescue TypeError
126
- next
127
- rescue NoMethodError
128
- next
129
- rescue KeyError
130
- next
143
+ if Crystalline.union_strategy == :populated_fields
144
+ to_build[key] = Crystalline.unmarshal_union_populated_fields(value, union_types)
145
+ else
146
+ union_types.each do |union_type|
147
+ begin
148
+ to_build[key] = Crystalline.unmarshal_json(value, union_type)
149
+ rescue TypeError
150
+ next
151
+ rescue NoMethodError
152
+ next
153
+ rescue KeyError
154
+ next
155
+ end
156
+ break
131
157
  end
132
- break
133
158
  end
134
159
  end
135
160
  elsif field_type.instance_of?(Class) && field_type.include?(::Crystalline::MetadataFields)
@@ -138,6 +163,32 @@ module Crystalline
138
163
  to_build[key] = unmarshal_single(field_type, value, format_metadata)
139
164
  end
140
165
  end
166
+
167
+ # Process additional properties fields: collect remaining keys from the dict
168
+ additional_props_fields.each do |field|
169
+ key = field.name
170
+ format_metadata = field.metadata.fetch(:format_json, {})
171
+ field_type = field.type
172
+
173
+ remaining = d.reject { |k, _| known_keys.key?(k) }
174
+ if remaining.empty?
175
+ next if ::Crystalline::Utils.nilable?(field.type)
176
+
177
+ raise KeyError, 'no additional properties found in hash'
178
+ end
179
+
180
+ inner_field_type = field_type
181
+ inner_field_type = ::Crystalline::Utils.nilable_of(inner_field_type) if ::Crystalline::Utils.nilable?(inner_field_type)
182
+ if Crystalline::Utils.hash?(inner_field_type)
183
+ val_type = Crystalline::Utils.hash_of(inner_field_type)
184
+ # rubocop:disable Style/HashTransformValues
185
+ to_build[key] = remaining.map { |k, v| [k, unmarshal_single(val_type, v, format_metadata)] }.to_h
186
+ # rubocop:enable Style/HashTransformValues
187
+ else
188
+ to_build[key] = remaining
189
+ end
190
+ end
191
+
141
192
  new(**to_build)
142
193
  end
143
194
  end
@@ -179,6 +230,8 @@ module Crystalline
179
230
  fields.sort_by(&:name).each do |field|
180
231
  format_json_meta = field.metadata[:format_json]
181
232
  required = !format_json_meta.nil? && format_json_meta.include?(:required)
233
+ is_additional_props = !format_json_meta.nil? && format_json_meta.fetch(:additional_properties, false)
234
+
182
235
  if !format_json_meta.nil? && format_json_meta.include?(:letter_case)
183
236
  key = format_json_meta[:letter_case].call(field.name)
184
237
  else
@@ -189,10 +242,13 @@ module Crystalline
189
242
  next if f.nil? && !required
190
243
  result[key] = nil if f.nil? && required
191
244
 
192
- if f.is_a? Array
245
+ # Flatten additional properties into the parent object
246
+ if is_additional_props && f.is_a?(::Hash)
247
+ f.each { |k, v| result[k] = marshal_single(v) }
248
+ elsif f.is_a? ::Array
193
249
  result[key] = f.map { |o| marshal_single(o) }
194
- elsif f.is_a? Hash
195
- result[key] = f.map { |k, v| [k, marshal_single(v)] }
250
+ elsif f.is_a? ::Hash
251
+ result[key] = f.transform_values { |v| marshal_single(v) }
196
252
  else
197
253
  result[key] = marshal_single(f)
198
254
  end
@@ -4,11 +4,16 @@
4
4
  # frozen_string_literal: true
5
5
 
6
6
  module Crystalline
7
+ @union_strategy = :left_to_right
8
+
9
+ class << self
10
+ attr_accessor :union_strategy
11
+ end
7
12
 
8
13
  def self.to_dict(complex)
9
- if complex.is_a? Array
14
+ if complex.is_a? ::Array
10
15
  complex.map { |v| Crystalline.to_dict(v) }
11
- elsif complex.is_a? Hash
16
+ elsif complex.is_a? ::Hash
12
17
  complex.transform_values { |v| Crystalline.to_dict(v) }
13
18
  elsif complex.respond_to?(:class) && complex.class.include?(::Crystalline::MetadataFields)
14
19
  complex.to_dict
@@ -29,21 +34,18 @@ module Crystalline
29
34
  if Crystalline::Utils.nilable? type
30
35
  type = Crystalline::Utils.nilable_of type
31
36
  end
32
- if type.instance_of?(Class) && type.include?(::Crystalline::MetadataFields)
37
+ if type.is_a?(Crystalline::DiscriminatedUnion)
38
+ type.parse(data)
39
+ elsif type.instance_of?(Class) && type.include?(::Crystalline::MetadataFields)
33
40
  type.from_dict(data)
34
41
  elsif Crystalline::Utils.union? type
35
42
  union_types = Crystalline::Utils.get_union_types(type)
36
43
  union_types = union_types.sort_by { |klass| Crystalline.non_nilable_attr_count(klass) }
37
44
 
38
- union_types.each do |union_type|
39
- unmarshalled_val = Crystalline.unmarshal_json(data, union_type)
40
- return unmarshalled_val
41
- rescue TypeError
42
- next
43
- rescue NoMethodError
44
- next
45
- rescue KeyError
46
- next
45
+ if Crystalline.union_strategy == :populated_fields
46
+ Crystalline.unmarshal_union_populated_fields(data, union_types)
47
+ else
48
+ Crystalline.unmarshal_union_left_to_right(data, union_types)
47
49
  end
48
50
  elsif Crystalline::Utils.arr? type
49
51
  data.map { |v| Crystalline.unmarshal_json(v, Crystalline::Utils.arr_of(type)) }
@@ -51,6 +53,12 @@ module Crystalline
51
53
  data.transform_values { |v| Crystalline.unmarshal_json(v, Crystalline::Utils.hash_of(type)) }
52
54
  elsif Crystalline::Utils.nilable?(type) && data == 'null'
53
55
  nil
56
+ elsif Crystalline::Utils.boolean? type
57
+ Crystalline::Utils.to_boolean(data)
58
+ elsif type.is_a?(Class) && type < T::Enum
59
+ type.deserialize(data)
60
+ elsif type.is_a?(Class) && type.respond_to?(:enums) && type.respond_to?(:deserialize)
61
+ type.deserialize(data)
54
62
  else
55
63
  data
56
64
  end
@@ -60,7 +68,7 @@ module Crystalline
60
68
  if val.class.respond_to? :enums
61
69
  val.serialize
62
70
  elsif val.is_a? DateTime
63
- val.strftime('%Y-%m-%dT%H:%M:%S.%NZ')
71
+ val.strftime('%Y-%m-%dT%H:%M:%S.%NZ').sub(/(\.\d*[1-9])0+Z\z/, '\1Z').sub(/\.0+Z\z/, 'Z')
64
72
  elsif val.nil?
65
73
  nil
66
74
  elsif primitives
@@ -70,6 +78,88 @@ module Crystalline
70
78
  end
71
79
  end
72
80
 
81
+ def self.unmarshal_union_left_to_right(data, union_types)
82
+ union_types.each do |union_type|
83
+ return Crystalline.unmarshal_json(data, union_type)
84
+ rescue TypeError
85
+ next
86
+ rescue NoMethodError
87
+ next
88
+ rescue KeyError
89
+ next
90
+ end
91
+ nil
92
+ end
93
+
94
+ def self.unmarshal_union_populated_fields(data, union_types)
95
+ best_value = nil
96
+ best_matched = -1
97
+ best_inexact = 0
98
+ best_unmatched = 0
99
+
100
+ union_types.each do |union_type|
101
+ value = Crystalline.unmarshal_json(data, union_type)
102
+ matched, inexact, unmatched = count_matched_fields(value, data)
103
+
104
+ if best_value.nil? ||
105
+ matched > best_matched ||
106
+ (matched == best_matched && inexact < best_inexact) ||
107
+ (matched == best_matched && inexact == best_inexact && unmatched < best_unmatched)
108
+ best_value = value
109
+ best_matched = matched
110
+ best_inexact = inexact
111
+ best_unmatched = unmatched
112
+ end
113
+ rescue TypeError
114
+ next
115
+ rescue NoMethodError
116
+ next
117
+ rescue KeyError
118
+ next
119
+ end
120
+ best_value
121
+ end
122
+
123
+ def self.count_matched_fields(value, raw_data)
124
+ matched = 0
125
+ inexact = 0
126
+ unmatched = 0
127
+
128
+ if value.is_a?(Crystalline::Unknown)
129
+ return [0, 0, 0]
130
+ elsif value.class.include?(::Crystalline::MetadataFields)
131
+ value.fields.each do |field|
132
+ format_metadata = field.metadata.fetch(:format_json, {})
133
+ lookup = format_metadata.fetch(:letter_case, nil)&.call
134
+ field_val = value.send(field.name)
135
+
136
+ if raw_data.is_a?(::Hash) && lookup && raw_data.key?(lookup)
137
+ if field_val.class.include?(::Crystalline::MetadataFields) && raw_data[lookup].is_a?(::Hash)
138
+ nested_matched, nested_inexact, nested_unmatched = count_matched_fields(field_val, raw_data[lookup])
139
+ matched += nested_matched
140
+ inexact += nested_inexact
141
+ unmatched += nested_unmatched
142
+ else
143
+ matched += 1
144
+ if field_val.respond_to?(:known?) && !field_val.known?
145
+ inexact += 1
146
+ end
147
+ end
148
+ elsif !::Crystalline::Utils.nilable?(field.type)
149
+ unmatched += 1
150
+ end
151
+ end
152
+ elsif value.is_a?(::Array)
153
+ matched = value.length
154
+ elsif value.is_a?(::Hash)
155
+ matched = value.length
156
+ else
157
+ matched = 1
158
+ end
159
+
160
+ [matched, inexact, unmatched]
161
+ end
162
+
73
163
  def self.non_nilable_attr_count(klass)
74
164
  # somewhat sane sort ordering for Union deserialization.
75
165
  # All Crystalline objects return the number of non-nilable fields
@@ -40,26 +40,101 @@ module Crystalline
40
40
  class Boolean
41
41
  end
42
42
 
43
+ # Wraps an unrecognized payload from an open discriminated union.
44
+ # Produced when the discriminator value is missing, unknown, or schema validation fails.
45
+ class Unknown
46
+ attr_reader :raw
47
+
48
+ def initialize(raw:)
49
+ @raw = raw
50
+ end
51
+
52
+ def unknown?
53
+ true
54
+ end
55
+ end
56
+
57
+ # Forward-compatible discriminated union parser.
58
+ # Known discriminator values are deserialized to their mapped types.
59
+ # Unknown or invalid payloads are captured as Crystalline::Unknown.
60
+ class DiscriminatedUnion
61
+ attr_reader :discriminator, :variants
62
+
63
+ def initialize(discriminator, variants)
64
+ @discriminator = discriminator
65
+ @variants = variants
66
+ end
67
+
68
+ def parse(payload)
69
+ unless payload.is_a?(::Hash)
70
+ return Unknown.new(raw: payload)
71
+ end
72
+
73
+ disc_value = payload[@discriminator]
74
+ unless disc_value.is_a?(::String)
75
+ return Unknown.new(raw: payload)
76
+ end
77
+
78
+ variant_type = @variants[disc_value]
79
+ unless variant_type
80
+ return Unknown.new(raw: payload)
81
+ end
82
+
83
+ begin
84
+ Crystalline.unmarshal_json(payload, variant_type)
85
+ rescue StandardError
86
+ Unknown.new(raw: payload)
87
+ end
88
+ end
89
+ end
90
+
91
+ def self.unknown?(value)
92
+ value.is_a?(Unknown)
93
+ end
94
+
43
95
  module Enum
96
+ def self.included(base)
97
+ base.extend(ClassMethods)
98
+ end
99
+
44
100
  def initialize(val)
45
- puts methods
46
- if instance_methods(false).include?(:initialize)
101
+ if self.class.instance_methods(false).include?(:initialize)
47
102
  super(val)
48
103
  else
49
104
  @val = val
50
105
  end
51
106
  end
52
107
 
53
- def self.enums(&blk)
54
- @mapping = {}
108
+ module ClassMethods
109
+ def enums(&blk)
110
+ @mapping = {}
55
111
 
56
- yield
57
- constants(false).each do |const_name|
58
- instance = const_get(const_name, false)
59
- unless instance.is_a? self
60
- raise 'Enum constants must be instances of the Enum class (e.g. `Foo = new`)'
112
+ yield
113
+ constants(false).each do |const_name|
114
+ instance = const_get(const_name, false)
115
+ unless instance.is_a? self
116
+ raise 'Enum constants must be instances of the Enum class (e.g. `Foo = new`)'
117
+ end
118
+ @mapping[instance.serialize] = instance
119
+ end
120
+ end
121
+
122
+ def open!
123
+ @open = true
124
+ end
125
+
126
+ def open?
127
+ @open == true
128
+ end
129
+
130
+ def deserialize(val)
131
+ if @mapping.include? val
132
+ @mapping[val]
133
+ elsif open?
134
+ new(val)
135
+ else
136
+ raise "Invalid value for enum: #{val}"
61
137
  end
62
- @mapping[instance.serialize] = instance
63
138
  end
64
139
  end
65
140
 
@@ -67,12 +142,21 @@ module Crystalline
67
142
  @val
68
143
  end
69
144
 
70
- def deserialize(val)
71
- if @mapping.include? val
72
- @mapping[val]
73
- else
74
- raise "Invalid value for enum: #{val}"
75
- end
145
+ def known?
146
+ self.class.instance_variable_get(:@mapping)&.value?(self) || false
147
+ end
148
+
149
+ def ==(other)
150
+ other = other.serialize if other.is_a?(self.class)
151
+ @val == other
152
+ end
153
+
154
+ def eql?(other)
155
+ self == other
156
+ end
157
+
158
+ def hash
159
+ @val.hash
76
160
  end
77
161
  end
78
162
  end
@@ -52,5 +52,18 @@ module Crystalline
52
52
  def self.get_union_types(t)
53
53
  t.types
54
54
  end
55
+
56
+ def self.boolean?(t)
57
+ t.instance_of? Crystalline::Boolean
58
+ end
59
+
60
+ def self.to_boolean(value)
61
+ case value
62
+ when true, false
63
+ value
64
+ else
65
+ raise TypeError, "Cannot convert #{value.inspect} to boolean. The value is of type #{value.class}"
66
+ end
67
+ end
55
68
  end
56
69
  end
@@ -55,7 +55,7 @@ module KintsugiSDK
55
55
  headers['content-type'] = req_content_type
56
56
  raise StandardError, 'request body is required' if data.nil? && form.nil?
57
57
 
58
- if form
58
+ if form && !form.empty?
59
59
  body = Utils.encode_form(form)
60
60
  elsif Utils.match_content_type(req_content_type, 'application/x-www-form-urlencoded')
61
61
  body = URI.encode_www_form(T.cast(data, T::Hash[Symbol, Object]))
@@ -209,7 +209,7 @@ module KintsugiSDK
209
209
  headers['content-type'] = req_content_type
210
210
  raise StandardError, 'request body is required' if data.nil? && form.nil?
211
211
 
212
- if form
212
+ if form && !form.empty?
213
213
  body = Utils.encode_form(form)
214
214
  elsif Utils.match_content_type(req_content_type, 'application/x-www-form-urlencoded')
215
215
  body = URI.encode_www_form(T.cast(data, T::Hash[Symbol, Object]))
@@ -230,7 +230,7 @@ module KintsugiSDK
230
230
  hook_ctx = SDKHooks::HookContext.new(
231
231
  config: @sdk_configuration,
232
232
  base_url: base_url,
233
- oauth2_scopes: [],
233
+ oauth2_scopes: nil,
234
234
  operation_id: 'suggestions_v1_address_validation_suggestions_post',
235
235
  security_source: @sdk_configuration.security_source
236
236
  )
@@ -65,7 +65,7 @@ module KintsugiSDK
65
65
  hook_ctx = SDKHooks::HookContext.new(
66
66
  config: @sdk_configuration,
67
67
  base_url: base_url,
68
- oauth2_scopes: [],
68
+ oauth2_scopes: nil,
69
69
  operation_id: 'get_customers_v1',
70
70
  security_source: @sdk_configuration.security_source
71
71
  )
@@ -197,7 +197,7 @@ module KintsugiSDK
197
197
  headers['content-type'] = req_content_type
198
198
  raise StandardError, 'request body is required' if data.nil? && form.nil?
199
199
 
200
- if form
200
+ if form && !form.empty?
201
201
  body = Utils.encode_form(form)
202
202
  elsif Utils.match_content_type(req_content_type, 'application/x-www-form-urlencoded')
203
203
  body = URI.encode_www_form(T.cast(data, T::Hash[Symbol, Object]))
@@ -218,7 +218,7 @@ module KintsugiSDK
218
218
  hook_ctx = SDKHooks::HookContext.new(
219
219
  config: @sdk_configuration,
220
220
  base_url: base_url,
221
- oauth2_scopes: [],
221
+ oauth2_scopes: nil,
222
222
  operation_id: 'create_customer_v1_customers_post',
223
223
  security_source: @sdk_configuration.security_source
224
224
  )
@@ -366,7 +366,7 @@ module KintsugiSDK
366
366
  hook_ctx = SDKHooks::HookContext.new(
367
367
  config: @sdk_configuration,
368
368
  base_url: base_url,
369
- oauth2_scopes: [],
369
+ oauth2_scopes: nil,
370
370
  operation_id: 'get_customer_by_id_v1_customers__customer_id__get',
371
371
  security_source: @sdk_configuration.security_source
372
372
  )
@@ -475,7 +475,7 @@ module KintsugiSDK
475
475
  headers['content-type'] = req_content_type
476
476
  raise StandardError, 'request body is required' if data.nil? && form.nil?
477
477
 
478
- if form
478
+ if form && !form.empty?
479
479
  body = Utils.encode_form(form)
480
480
  elsif Utils.match_content_type(req_content_type, 'application/x-www-form-urlencoded')
481
481
  body = URI.encode_www_form(T.cast(data, T::Hash[Symbol, Object]))
@@ -496,7 +496,7 @@ module KintsugiSDK
496
496
  hook_ctx = SDKHooks::HookContext.new(
497
497
  config: @sdk_configuration,
498
498
  base_url: base_url,
499
- oauth2_scopes: [],
499
+ oauth2_scopes: nil,
500
500
  operation_id: 'update_customer_v1_customers__customer_id__put',
501
501
  security_source: @sdk_configuration.security_source
502
502
  )
@@ -644,7 +644,7 @@ module KintsugiSDK
644
644
  hook_ctx = SDKHooks::HookContext.new(
645
645
  config: @sdk_configuration,
646
646
  base_url: base_url,
647
- oauth2_scopes: [],
647
+ oauth2_scopes: nil,
648
648
  operation_id: 'get_customer_by_external_id_v1_customers_external__external_id__get',
649
649
  security_source: @sdk_configuration.security_source
650
650
  )
@@ -761,7 +761,7 @@ module KintsugiSDK
761
761
  hook_ctx = SDKHooks::HookContext.new(
762
762
  config: @sdk_configuration,
763
763
  base_url: base_url,
764
- oauth2_scopes: [],
764
+ oauth2_scopes: nil,
765
765
  operation_id: 'getTransactionsByCustomer',
766
766
  security_source: @sdk_configuration.security_source
767
767
  )
@@ -868,7 +868,7 @@ module KintsugiSDK
868
868
  headers['content-type'] = req_content_type
869
869
  raise StandardError, 'request body is required' if data.nil? && form.nil?
870
870
 
871
- if form
871
+ if form && !form.empty?
872
872
  body = Utils.encode_form(form)
873
873
  elsif Utils.match_content_type(req_content_type, 'application/x-www-form-urlencoded')
874
874
  body = URI.encode_www_form(T.cast(data, T::Hash[Symbol, Object]))
@@ -889,7 +889,7 @@ module KintsugiSDK
889
889
  hook_ctx = SDKHooks::HookContext.new(
890
890
  config: @sdk_configuration,
891
891
  base_url: base_url,
892
- oauth2_scopes: [],
892
+ oauth2_scopes: nil,
893
893
  operation_id: 'createTransactionByCustomer',
894
894
  security_source: @sdk_configuration.security_source
895
895
  )