xero-kiwi 0.5.0 → 0.5.2
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 +4 -4
- data/CHANGELOG.md +14 -0
- data/Rakefile +22 -63
- data/docs/client.md +111 -9
- data/lib/xero_kiwi/accounting/resource.rb +26 -3
- data/lib/xero_kiwi/version.rb +1 -1
- data/llms-full.txt +1849 -1747
- data/llms.txt +24 -10
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fb7181193042ba51598251bc4bee70c0e84ae0333216d94838e996f813b05770
|
|
4
|
+
data.tar.gz: 7103cdb42e4cb742cbafa0eae6c807f7958626751dbdd1cd86aa14fbbe4939a8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f5a369e373c45e9d4343666eb59e2adbf16a1620efe57052b89f655be7d09bc63c30d120c7a47dc873e5ed0ac1f6af5fb2b23c4b71847819ac15c9f3c2e0f5a3
|
|
7
|
+
data.tar.gz: 51ac21d3ace42a9ebefaa03ee2fdf924a6dd9549eb41d053901f0a2c6d115147e35a9f90e77438241937ca2171fdbf6a295792bfbef4528e209b5211f1d598b4
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.5.2] - 2026-09-27
|
|
4
|
+
|
|
5
|
+
### Documentation
|
|
6
|
+
|
|
7
|
+
- Documented that `Resource#raw` returns nil on nested objects (line items, addresses, contact persons, payment terms) even with `retain_raw: true`, and that the enclosing resource's hash is the access path — `org.raw["Addresses"]` rather than `org.addresses.map(&:raw)`. The behaviour was deliberate and explained in a comment on `from_response`, but the `#raw` docstring never mentioned it and the failure is a silent nil, so a consumer mapped over a nested collection and was one step from storing `[null]`.
|
|
8
|
+
- Added a written contract for `raw` in `docs/client.md`: it is the parsed JSON with only top-level keys stringified, kiwi never normalises it (absent keys stay absent rather than becoming `[]`), unmodelled keys are preserved, and the freeze is shallow — the hash is frozen, the structures inside it are not. Specs pin all four, plus the nested-nil behaviour. The previous "is frozen" example overstated the guarantee and has been replaced.
|
|
9
|
+
- `llms.txt` is now generated alongside `llms-full.txt` from a single manifest in `tasks/llms.rb`, rather than hand-maintained. It had not been touched since the initial commit and was missing four releases of material — no `querying.md`, no `throttling.md`, no `tracking-category.md`, and no mention of `Page`, `RateLimit`, the throttle limiters or the tracking classes. `rake llms:check` now also fails when a doc exists under `docs/` that the manifest does not list, which is the half that was missing: the freshness check could not notice a doc that was never indexed in the first place.
|
|
10
|
+
|
|
11
|
+
## [0.5.1] - 2026-09-26
|
|
12
|
+
|
|
13
|
+
### Documentation
|
|
14
|
+
|
|
15
|
+
- 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.
|
|
16
|
+
|
|
3
17
|
## [0.5.0] - 2026-09-26
|
|
4
18
|
|
|
5
19
|
The sync-support release: everything needed to drive a full-tenant sync through kiwi rather than around it.
|
data/Rakefile
CHANGED
|
@@ -9,76 +9,35 @@ require "rubocop/rake_task"
|
|
|
9
9
|
|
|
10
10
|
RuboCop::RakeTask.new
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
# the docs in the same order the README's table of contents lists them.
|
|
14
|
-
LLMS_SOURCE_FILES = %w[
|
|
15
|
-
README.md
|
|
16
|
-
docs/getting-started.md
|
|
17
|
-
docs/client.md
|
|
18
|
-
docs/oauth.md
|
|
19
|
-
docs/tokens.md
|
|
20
|
-
docs/connections.md
|
|
21
|
-
docs/accounting/contact.md
|
|
22
|
-
docs/accounting/contact-group.md
|
|
23
|
-
docs/accounting/organisation.md
|
|
24
|
-
docs/accounting/user.md
|
|
25
|
-
docs/accounting/credit-note.md
|
|
26
|
-
docs/accounting/invoice.md
|
|
27
|
-
docs/accounting/payment.md
|
|
28
|
-
docs/accounting/overpayment.md
|
|
29
|
-
docs/accounting/prepayment.md
|
|
30
|
-
docs/accounting/branding-theme.md
|
|
31
|
-
docs/accounting/tracking-category.md
|
|
32
|
-
docs/accounting/address.md
|
|
33
|
-
docs/accounting/phone.md
|
|
34
|
-
docs/accounting/external-link.md
|
|
35
|
-
docs/accounting/payment-terms.md
|
|
36
|
-
docs/errors.md
|
|
37
|
-
docs/retries-and-rate-limits.md
|
|
38
|
-
docs/throttling.md
|
|
39
|
-
docs/querying.md
|
|
40
|
-
].freeze
|
|
41
|
-
|
|
42
|
-
LLMS_FULL_PATH = "llms-full.txt"
|
|
43
|
-
|
|
44
|
-
# Builds the llms-full.txt content from LLMS_SOURCE_FILES. Pure function:
|
|
45
|
-
# returns a String, doesn't touch the filesystem. Both `llms:build` and
|
|
46
|
-
# `llms:check` use this so the two tasks can never disagree about the
|
|
47
|
-
# expected output.
|
|
48
|
-
def build_llms_full
|
|
49
|
-
out = String.new(encoding: "UTF-8")
|
|
50
|
-
out << "# Xero Kiwi — full documentation\n\n"
|
|
51
|
-
out << "This file is the complete documentation for the Xero Kiwi gem (a Ruby wrapper for the Xero Accounting API), assembled into a single document for LLM consumption. It contains the README and every doc in the docs/ folder, in reading order.\n\n"
|
|
52
|
-
out << "For the curated index version, see llms.txt in the same directory.\n\n"
|
|
53
|
-
out << "Source: https://github.com/douglasgreyling/xero-kiwi\n\n"
|
|
54
|
-
LLMS_SOURCE_FILES.each { |path| append_file_block(out, path) }
|
|
55
|
-
out
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def append_file_block(out, path)
|
|
59
|
-
separator = "=" * 80
|
|
60
|
-
out << "\n" << separator << "\n"
|
|
61
|
-
out << "FILE: #{path}\n"
|
|
62
|
-
out << separator << "\n\n"
|
|
63
|
-
out << File.read(path, encoding: "UTF-8") << "\n"
|
|
64
|
-
end
|
|
12
|
+
require_relative "tasks/llms"
|
|
65
13
|
|
|
66
14
|
namespace :llms do
|
|
67
|
-
desc "Regenerate llms-full.txt from README and docs/"
|
|
15
|
+
desc "Regenerate llms.txt and llms-full.txt from README and docs/"
|
|
68
16
|
task :build do
|
|
69
|
-
|
|
70
|
-
|
|
17
|
+
Llms.outputs.each do |path, content|
|
|
18
|
+
File.write(path, content)
|
|
19
|
+
puts "Wrote #{path} (#{File.size(path)} bytes)"
|
|
20
|
+
end
|
|
71
21
|
end
|
|
72
22
|
|
|
73
|
-
desc "Verify llms-full.txt
|
|
23
|
+
desc "Verify llms.txt and llms-full.txt are up to date, and that every doc is indexed"
|
|
74
24
|
task :check do
|
|
75
|
-
|
|
76
|
-
|
|
25
|
+
missing = Llms.unindexed_docs
|
|
26
|
+
unless missing.empty?
|
|
27
|
+
warn "✗ These docs are not listed in Llms::DOCS, so they are missing from llms.txt and llms-full.txt:"
|
|
28
|
+
missing.each { |path| warn " #{path}" }
|
|
29
|
+
warn " Add them to tasks/llms.rb and run `bundle exec rake llms:build`."
|
|
30
|
+
exit 1
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
stale = Llms.outputs.reject do |path, expected|
|
|
34
|
+
File.exist?(path) && File.read(path, encoding: "UTF-8") == expected
|
|
35
|
+
end
|
|
77
36
|
|
|
78
|
-
if
|
|
79
|
-
puts "✓
|
|
37
|
+
if stale.empty?
|
|
38
|
+
puts "✓ llms.txt and llms-full.txt are up to date"
|
|
80
39
|
else
|
|
81
|
-
warn "✗
|
|
40
|
+
warn "✗ Out of date with README/docs: #{stale.keys.join(", ")}"
|
|
82
41
|
warn " Run `bundle exec rake llms:build` and commit the result."
|
|
83
42
|
exit 1
|
|
84
43
|
end
|
|
@@ -86,7 +45,7 @@ namespace :llms do
|
|
|
86
45
|
end
|
|
87
46
|
|
|
88
47
|
# Top-level convenience alias.
|
|
89
|
-
desc "Regenerate
|
|
48
|
+
desc "Regenerate the LLM docs bundles (alias for llms:build)"
|
|
90
49
|
task llms: "llms:build"
|
|
91
50
|
|
|
92
51
|
task default: %i[spec rubocop llms:check]
|
data/docs/client.md
CHANGED
|
@@ -59,23 +59,125 @@ contact.raw
|
|
|
59
59
|
# => {"ContactID" => "…", "ContactPersons" => [...], "SomeNewField" => "…"}
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
Use it to reach fields kiwi doesn't model yet
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
+
- **Nested objects return `nil`, even with `retain_raw: true`.** Line
|
|
72
|
+
items, addresses, contact persons and payment terms do not carry their
|
|
73
|
+
own payload. Reach them through the enclosing resource's hash:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
org.addresses.map(&:raw) # => [nil] ← not what you want
|
|
77
|
+
org.raw["Addresses"] # => [{"AddressType" => "POBOX", …}]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
This is deliberate — see [why nested objects don't carry
|
|
81
|
+
raw](#why-nested-objects-dont-carry-raw) — but it fails quietly, so it's
|
|
82
|
+
worth knowing before you map over a collection and store the result.
|
|
83
|
+
- **It's the JSON representation.** Kiwi sends
|
|
84
|
+
`Accept: application/json`. Xero also serves XML, which nests
|
|
85
|
+
differently — XML has no arrays, so a single child parses to a Hash and
|
|
86
|
+
several to an Array, where JSON is always an Array. `raw` cannot
|
|
87
|
+
reproduce an XML-derived shape. See [migrating from an XML
|
|
88
|
+
client](#migrating-from-an-xml-based-client) below.
|
|
67
89
|
- **`#raw` is not `#to_h`.** `to_h` is a snake_case projection rebuilt from
|
|
68
90
|
the modelled attributes — different keys, different nesting. If you store
|
|
69
91
|
`to_h` where you meant to store the payload, readers fail by returning
|
|
70
92
|
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.
|
|
75
93
|
- **It costs memory.** Every resource holds its source hash alongside the
|
|
76
94
|
hydrated attributes, which roughly doubles the footprint of a large page.
|
|
77
95
|
That's why it's off by default.
|
|
78
96
|
|
|
97
|
+
### What `raw` guarantees
|
|
98
|
+
|
|
99
|
+
Depend on these; they won't change without a major version.
|
|
100
|
+
|
|
101
|
+
- It is the parsed JSON for that resource, with **only the top-level keys
|
|
102
|
+
stringified**. Everything nested is exactly what `JSON.parse` produced.
|
|
103
|
+
- **Kiwi does not normalise it.** No key renaming, no defaulting, no
|
|
104
|
+
filling in of absent keys. If Xero omits `Addresses`, `raw` has no
|
|
105
|
+
`"Addresses"` key — it does not become `[]`. If Xero sends `[]`, you get
|
|
106
|
+
`[]`. `raw.fetch("Addresses", [])` and `raw["Addresses"] || []` are both
|
|
107
|
+
safe and mean the same thing here.
|
|
108
|
+
- Keys the gem doesn't model are preserved. That's the point of it.
|
|
109
|
+
- The top-level hash is **frozen; nested structures are not**. `raw` itself
|
|
110
|
+
can't be mutated, but `raw["Addresses"]` is an ordinary mutable Array.
|
|
111
|
+
Dup before mutating if that matters to you.
|
|
112
|
+
|
|
113
|
+
The one thing deliberately *not* guaranteed is the presence of `raw` on
|
|
114
|
+
nested objects, per the bullet above.
|
|
115
|
+
|
|
116
|
+
### Why nested objects don't carry raw
|
|
117
|
+
|
|
118
|
+
Two reasons, both worth knowing if you're tempted to ask for it.
|
|
119
|
+
|
|
120
|
+
It would cost real memory on the workload `retain_raw` exists for. A page
|
|
121
|
+
of 1,000 invoices with five line items each retains about 3.4 MB with
|
|
122
|
+
top-level raw; giving every nested object its own hash adds about 1.6 MB
|
|
123
|
+
on top, roughly half again, and that's on a flag that has already doubled
|
|
124
|
+
your footprint.
|
|
125
|
+
|
|
126
|
+
More importantly it couldn't be done consistently. Several attributes —
|
|
127
|
+
`payment_terms` on Contact and Organisation, `invoice_addresses` on
|
|
128
|
+
Invoice — hydrate through custom lambdas that construct their objects
|
|
129
|
+
directly, so they'd stay `nil` while their siblings worked. "Nested
|
|
130
|
+
objects never carry raw" is a rule you can hold in your head. "Nested
|
|
131
|
+
objects carry raw except these four" is not.
|
|
132
|
+
|
|
133
|
+
### Migrating from an XML-based client
|
|
134
|
+
|
|
135
|
+
If you're replacing a Xero client that sent `Accept: text/xml` — HTTParty
|
|
136
|
+
and similar default to it — any payloads you already have stored are
|
|
137
|
+
XML-shaped, and `raw` will not match them. The differences are structural,
|
|
138
|
+
not cosmetic:
|
|
139
|
+
|
|
140
|
+
| | XML (`text/xml`) | JSON (`application/json`) |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| Organisation body | `{"Organisations" => {"Organisation" => {…}}}` | `{"Organisations" => [{…}]}` |
|
|
143
|
+
| One address | `{"Address" => {…}}` | `[{…}]` |
|
|
144
|
+
| Several addresses | `{"Address" => [{…}, {…}]}` | `[{…}, {…}]` |
|
|
145
|
+
|
|
146
|
+
The single-child collapse is the one that catches people: under XML a
|
|
147
|
+
contact with one person parses to a Hash and a contact with two parses to
|
|
148
|
+
an Array, from the same endpoint. Code written against that has a
|
|
149
|
+
normalising step somewhere, whether or not its author knew why.
|
|
150
|
+
|
|
151
|
+
No client setting reproduces these shapes — they're artefacts of an XML
|
|
152
|
+
parse kiwi doesn't do. But the remedy isn't the same everywhere, and it's
|
|
153
|
+
worth sorting your readers into two piles before planning the work.
|
|
154
|
+
|
|
155
|
+
**Readers that dig the XML-only structure have to change.** Something like
|
|
156
|
+
`dig("Organisations", "Organisation", "Addresses", "Address")` returns nil
|
|
157
|
+
against anything kiwi produces, whether you store `raw` or `to_h`.
|
|
158
|
+
Promote the fields those readers need to real columns and use `raw` for
|
|
159
|
+
the backfill — it's the true payload, so it carries everything required to
|
|
160
|
+
populate them.
|
|
161
|
+
|
|
162
|
+
**Readers that already normalise usually survive untouched.** A reader
|
|
163
|
+
doing `[value].flatten.compact` handles the Hash case, the Array case and
|
|
164
|
+
nil identically, so a JSON array flows straight through. If the keys it
|
|
165
|
+
reads are the same in both representations, only the *writer* changes:
|
|
166
|
+
stop unwrapping, store the array. Existing rows stay readable, and you
|
|
167
|
+
skip a migration entirely.
|
|
168
|
+
|
|
169
|
+
Type coercion is usually fine in that second pile too. XML gives you
|
|
170
|
+
strings — `"false"` rather than `false` — and if the reader hands that to
|
|
171
|
+
something like ActiveModel's boolean cast, both the old string and the new
|
|
172
|
+
real boolean land on the same value.
|
|
173
|
+
|
|
174
|
+
One piece of luck worth knowing about: the two piles tend to fail
|
|
175
|
+
differently. A `dig` that misses returns nil and writes a blank record
|
|
176
|
+
quietly. Code that assumed a Hash, such as `Array#to_h` on what is now a
|
|
177
|
+
list, raises `TypeError` on the first record with data in it. The noisy
|
|
178
|
+
failures are the ones you can trust to find themselves — budget your
|
|
179
|
+
review time for the silent ones.
|
|
180
|
+
|
|
79
181
|
## What the client gives you
|
|
80
182
|
|
|
81
183
|
| Method | Returns | Purpose |
|
|
@@ -135,9 +135,32 @@ module XeroKiwi
|
|
|
135
135
|
end
|
|
136
136
|
end
|
|
137
137
|
|
|
138
|
-
#
|
|
139
|
-
# unless the client was built with `retain_raw: true`.
|
|
140
|
-
# fields the gem doesn't model
|
|
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
|
+
# Returns nil on NESTED objects even when retain_raw is on — line
|
|
143
|
+
# items, addresses, contact persons, payment terms. Reach those
|
|
144
|
+
# through the enclosing resource instead:
|
|
145
|
+
#
|
|
146
|
+
# org.addresses.map(&:raw) # => [nil]
|
|
147
|
+
# org.raw["Addresses"] # => [{"AddressType" => "POBOX", ...}]
|
|
148
|
+
#
|
|
149
|
+
# Scope is the item, NOT the enclosing envelope: `contact.raw` has no
|
|
150
|
+
# "Contacts" key, and `organisation.raw` no "Organisations" key. Kiwi
|
|
151
|
+
# unwraps the envelope before a resource is built, so there is nothing
|
|
152
|
+
# left of it by the time this is populated.
|
|
153
|
+
#
|
|
154
|
+
# Kiwi never normalises what it stores here: absent keys stay absent
|
|
155
|
+
# rather than becoming [] or nil entries, and keys the gem doesn't
|
|
156
|
+
# model are preserved. The hash itself is frozen; the structures
|
|
157
|
+
# nested inside it are not.
|
|
158
|
+
#
|
|
159
|
+
# It is also the JSON representation specifically. Kiwi sends
|
|
160
|
+
# `Accept: application/json`; Xero's XML representation nests
|
|
161
|
+
# differently — no arrays, so one child is a Hash and several are an
|
|
162
|
+
# Array — and `raw` cannot reproduce that shape. Worth knowing when
|
|
163
|
+
# migrating off an XML-based Xero client with stored payloads.
|
|
141
164
|
#
|
|
142
165
|
# Note `to_h` is NOT this — it's a snake_case projection rebuilt from
|
|
143
166
|
# the modelled attributes, with different keys and different nesting.
|
data/lib/xero_kiwi/version.rb
CHANGED