xero-kiwi 0.5.0 → 0.5.1

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: b980ba3e8f03590d2ca887887d8509268bf87df52b882eef4730a9ce1512250c
4
- data.tar.gz: bdd4952e2b7d1fd3af201f2446d17d5cd7f2bfe86e58ee9002f4115cc5624240
3
+ metadata.gz: 9e67980bd7d7be1131ee07b55847cd4633a527eec4159103ba43551903f0870c
4
+ data.tar.gz: f06a30a5311f02be7d8695344a7f8eb65e76f4b0abab1743f23b9701f7655600
5
5
  SHA512:
6
- metadata.gz: e7086fef7948e63b5d9437a164a241f819e5a96beead2eb2701989613e8b95f643d805be209a63b98ab3dcd278c7e81f46165de054f1fd865318abb82a13e0e9
7
- data.tar.gz: 94c08345afefa1b130634ab9d643052d14a24ba5200ae60a7b324344706d71465e9cecb6ce30e183876dafdf7bde0fcdbbaea487869ea99073bb388f2a7f8ab2
6
+ metadata.gz: bf15925c5971526baeb6c3773de4f454ab917509200794b05957b27eabdfb4691e757a01f9defca50357cf0ce8ba7aa504b8e29973685c1428b365b28d80c5bf
7
+ data.tar.gz: 854a1cc894da5a8706e084f1b2e328a3529470f785e4cc22c27d80080ec7c2780cf3db40eb3f919fd9afd359539a879af82d241b6a2061f3673fa80ead876357
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.5.1] - 2026-09-26
4
+
5
+ ### Documentation
6
+
7
+ - Pinned down the scope of `Resource#raw`, which was ambiguous enough to mislead a real consumer. It holds the resource's own item hash — `contact.raw` has no `"Contacts"` key — and it is the JSON representation specifically. Xero's XML representation nests differently (no arrays, so one child parses to a Hash and several to an Array), so `raw` cannot reproduce an XML-derived shape. `docs/client.md` gains a short migration section covering what that means for anyone replacing a client that sent `Accept: text/xml` and has stored payloads. Specs added for both the envelope scope and the PascalCase/snake_case split. The migration section sorts readers into the two piles that behave differently: ones that dig the XML-only structure and must change, and ones that already normalise (`[value].flatten.compact`) where only the writer changes and stored rows stay readable.
8
+
3
9
  ## [0.5.0] - 2026-09-26
4
10
 
5
11
  The sync-support release: everything needed to drive a full-tenant sync through kiwi rather than around it.
data/docs/client.md CHANGED
@@ -59,23 +59,81 @@ contact.raw
59
59
  # => {"ContactID" => "…", "ContactPersons" => [...], "SomeNewField" => "…"}
60
60
  ```
61
61
 
62
- Use it to reach fields kiwi doesn't model yet, or to store what Xero sent
63
- verbatim.
64
-
65
- Three things to know:
66
-
62
+ Use it to reach fields kiwi doesn't model yet.
63
+
64
+ Five things to know:
65
+
66
+ - **It's the item, not the envelope.** `contact.raw` has no `"Contacts"`
67
+ key and `organisation.raw` has no `"Organisations"` key — kiwi unwraps
68
+ the envelope before building a resource, so there's nothing left of it by
69
+ the time `raw` is populated. If you need the envelope back, you're
70
+ rebuilding it yourself.
71
+ - **It's the JSON representation.** Kiwi sends
72
+ `Accept: application/json`. Xero also serves XML, which nests
73
+ differently — XML has no arrays, so a single child parses to a Hash and
74
+ several to an Array, where JSON is always an Array. `raw` cannot
75
+ reproduce an XML-derived shape. See [migrating from an XML
76
+ client](#migrating-from-an-xml-based-client) below.
67
77
  - **`#raw` is not `#to_h`.** `to_h` is a snake_case projection rebuilt from
68
78
  the modelled attributes — different keys, different nesting. If you store
69
79
  `to_h` where you meant to store the payload, readers fail by returning
70
80
  nil rather than raising.
71
- - **It's top-level only.** Nested objects (line items, addresses, contact
72
- persons) have no `#raw` of their own. They don't need one: the top-level
73
- hash already holds every nested payload, so `contact.raw["ContactPersons"]`
74
- gets there.
81
+ - **It's per resource, not per nested object.** Line items, addresses and
82
+ contact persons have no `#raw` of their own. They don't need one: the
83
+ resource's hash already holds every nested payload, so
84
+ `contact.raw["ContactPersons"]` gets there.
75
85
  - **It costs memory.** Every resource holds its source hash alongside the
76
86
  hydrated attributes, which roughly doubles the footprint of a large page.
77
87
  That's why it's off by default.
78
88
 
89
+ ### Migrating from an XML-based client
90
+
91
+ If you're replacing a Xero client that sent `Accept: text/xml` — HTTParty
92
+ and similar default to it — any payloads you already have stored are
93
+ XML-shaped, and `raw` will not match them. The differences are structural,
94
+ not cosmetic:
95
+
96
+ | | XML (`text/xml`) | JSON (`application/json`) |
97
+ |---|---|---|
98
+ | Organisation body | `{"Organisations" => {"Organisation" => {…}}}` | `{"Organisations" => [{…}]}` |
99
+ | One address | `{"Address" => {…}}` | `[{…}]` |
100
+ | Several addresses | `{"Address" => [{…}, {…}]}` | `[{…}, {…}]` |
101
+
102
+ The single-child collapse is the one that catches people: under XML a
103
+ contact with one person parses to a Hash and a contact with two parses to
104
+ an Array, from the same endpoint. Code written against that has a
105
+ normalising step somewhere, whether or not its author knew why.
106
+
107
+ No client setting reproduces these shapes — they're artefacts of an XML
108
+ parse kiwi doesn't do. But the remedy isn't the same everywhere, and it's
109
+ worth sorting your readers into two piles before planning the work.
110
+
111
+ **Readers that dig the XML-only structure have to change.** Something like
112
+ `dig("Organisations", "Organisation", "Addresses", "Address")` returns nil
113
+ against anything kiwi produces, whether you store `raw` or `to_h`.
114
+ Promote the fields those readers need to real columns and use `raw` for
115
+ the backfill — it's the true payload, so it carries everything required to
116
+ populate them.
117
+
118
+ **Readers that already normalise usually survive untouched.** A reader
119
+ doing `[value].flatten.compact` handles the Hash case, the Array case and
120
+ nil identically, so a JSON array flows straight through. If the keys it
121
+ reads are the same in both representations, only the *writer* changes:
122
+ stop unwrapping, store the array. Existing rows stay readable, and you
123
+ skip a migration entirely.
124
+
125
+ Type coercion is usually fine in that second pile too. XML gives you
126
+ strings — `"false"` rather than `false` — and if the reader hands that to
127
+ something like ActiveModel's boolean cast, both the old string and the new
128
+ real boolean land on the same value.
129
+
130
+ One piece of luck worth knowing about: the two piles tend to fail
131
+ differently. A `dig` that misses returns nil and writes a blank record
132
+ quietly. Code that assumed a Hash, such as `Array#to_h` on what is now a
133
+ list, raises `TypeError` on the first record with data in it. The noisy
134
+ failures are the ones you can trust to find themselves — budget your
135
+ review time for the silent ones.
136
+
79
137
  ## What the client gives you
80
138
 
81
139
  | Method | Returns | Purpose |
@@ -135,9 +135,20 @@ module XeroKiwi
135
135
  end
136
136
  end
137
137
 
138
- # Xero's response hash for this resource, exactly as it arrived, or nil
139
- # unless the client was built with `retain_raw: true`. Use it to reach
140
- # fields the gem doesn't model, or to store the payload verbatim.
138
+ # This resource's own hash from Xero's JSON response, exactly as it
139
+ # arrived, or nil unless the client was built with `retain_raw: true`.
140
+ # Use it to reach fields the gem doesn't model.
141
+ #
142
+ # Scope is the item, NOT the enclosing envelope: `contact.raw` has no
143
+ # "Contacts" key, and `organisation.raw` no "Organisations" key. Kiwi
144
+ # unwraps the envelope before a resource is built, so there is nothing
145
+ # left of it by the time this is populated.
146
+ #
147
+ # It is also the JSON representation specifically. Kiwi sends
148
+ # `Accept: application/json`; Xero's XML representation nests
149
+ # differently — no arrays, so one child is a Hash and several are an
150
+ # Array — and `raw` cannot reproduce that shape. Worth knowing when
151
+ # migrating off an XML-based Xero client with stored payloads.
141
152
  #
142
153
  # Note `to_h` is NOT this — it's a snake_case projection rebuilt from
143
154
  # the modelled attributes, with different keys and different nesting.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module XeroKiwi
4
- VERSION = "0.5.0"
4
+ VERSION = "0.5.1"
5
5
  end
data/llms-full.txt CHANGED
@@ -346,23 +346,81 @@ contact.raw
346
346
  # => {"ContactID" => "…", "ContactPersons" => [...], "SomeNewField" => "…"}
347
347
  ```
348
348
 
349
- Use it to reach fields kiwi doesn't model yet, or to store what Xero sent
350
- verbatim.
351
-
352
- Three things to know:
353
-
349
+ Use it to reach fields kiwi doesn't model yet.
350
+
351
+ Five things to know:
352
+
353
+ - **It's the item, not the envelope.** `contact.raw` has no `"Contacts"`
354
+ key and `organisation.raw` has no `"Organisations"` key — kiwi unwraps
355
+ the envelope before building a resource, so there's nothing left of it by
356
+ the time `raw` is populated. If you need the envelope back, you're
357
+ rebuilding it yourself.
358
+ - **It's the JSON representation.** Kiwi sends
359
+ `Accept: application/json`. Xero also serves XML, which nests
360
+ differently — XML has no arrays, so a single child parses to a Hash and
361
+ several to an Array, where JSON is always an Array. `raw` cannot
362
+ reproduce an XML-derived shape. See [migrating from an XML
363
+ client](#migrating-from-an-xml-based-client) below.
354
364
  - **`#raw` is not `#to_h`.** `to_h` is a snake_case projection rebuilt from
355
365
  the modelled attributes — different keys, different nesting. If you store
356
366
  `to_h` where you meant to store the payload, readers fail by returning
357
367
  nil rather than raising.
358
- - **It's top-level only.** Nested objects (line items, addresses, contact
359
- persons) have no `#raw` of their own. They don't need one: the top-level
360
- hash already holds every nested payload, so `contact.raw["ContactPersons"]`
361
- gets there.
368
+ - **It's per resource, not per nested object.** Line items, addresses and
369
+ contact persons have no `#raw` of their own. They don't need one: the
370
+ resource's hash already holds every nested payload, so
371
+ `contact.raw["ContactPersons"]` gets there.
362
372
  - **It costs memory.** Every resource holds its source hash alongside the
363
373
  hydrated attributes, which roughly doubles the footprint of a large page.
364
374
  That's why it's off by default.
365
375
 
376
+ ### Migrating from an XML-based client
377
+
378
+ If you're replacing a Xero client that sent `Accept: text/xml` — HTTParty
379
+ and similar default to it — any payloads you already have stored are
380
+ XML-shaped, and `raw` will not match them. The differences are structural,
381
+ not cosmetic:
382
+
383
+ | | XML (`text/xml`) | JSON (`application/json`) |
384
+ |---|---|---|
385
+ | Organisation body | `{"Organisations" => {"Organisation" => {…}}}` | `{"Organisations" => [{…}]}` |
386
+ | One address | `{"Address" => {…}}` | `[{…}]` |
387
+ | Several addresses | `{"Address" => [{…}, {…}]}` | `[{…}, {…}]` |
388
+
389
+ The single-child collapse is the one that catches people: under XML a
390
+ contact with one person parses to a Hash and a contact with two parses to
391
+ an Array, from the same endpoint. Code written against that has a
392
+ normalising step somewhere, whether or not its author knew why.
393
+
394
+ No client setting reproduces these shapes — they're artefacts of an XML
395
+ parse kiwi doesn't do. But the remedy isn't the same everywhere, and it's
396
+ worth sorting your readers into two piles before planning the work.
397
+
398
+ **Readers that dig the XML-only structure have to change.** Something like
399
+ `dig("Organisations", "Organisation", "Addresses", "Address")` returns nil
400
+ against anything kiwi produces, whether you store `raw` or `to_h`.
401
+ Promote the fields those readers need to real columns and use `raw` for
402
+ the backfill — it's the true payload, so it carries everything required to
403
+ populate them.
404
+
405
+ **Readers that already normalise usually survive untouched.** A reader
406
+ doing `[value].flatten.compact` handles the Hash case, the Array case and
407
+ nil identically, so a JSON array flows straight through. If the keys it
408
+ reads are the same in both representations, only the *writer* changes:
409
+ stop unwrapping, store the array. Existing rows stay readable, and you
410
+ skip a migration entirely.
411
+
412
+ Type coercion is usually fine in that second pile too. XML gives you
413
+ strings — `"false"` rather than `false` — and if the reader hands that to
414
+ something like ActiveModel's boolean cast, both the old string and the new
415
+ real boolean land on the same value.
416
+
417
+ One piece of luck worth knowing about: the two piles tend to fail
418
+ differently. A `dig` that misses returns nil and writes a blank record
419
+ quietly. Code that assumed a Hash, such as `Array#to_h` on what is now a
420
+ list, raises `TypeError` on the first record with data in it. The noisy
421
+ failures are the ones you can trust to find themselves — budget your
422
+ review time for the silent ones.
423
+
366
424
  ## What the client gives you
367
425
 
368
426
  | Method | Returns | Purpose |
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: xero-kiwi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Douglas Greyling