xero-kiwi 0.5.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e67980bd7d7be1131ee07b55847cd4633a527eec4159103ba43551903f0870c
4
- data.tar.gz: f06a30a5311f02be7d8695344a7f8eb65e76f4b0abab1743f23b9701f7655600
3
+ metadata.gz: fb7181193042ba51598251bc4bee70c0e84ae0333216d94838e996f813b05770
4
+ data.tar.gz: 7103cdb42e4cb742cbafa0eae6c807f7958626751dbdd1cd86aa14fbbe4939a8
5
5
  SHA512:
6
- metadata.gz: bf15925c5971526baeb6c3773de4f454ab917509200794b05957b27eabdfb4691e757a01f9defca50357cf0ce8ba7aa504b8e29973685c1428b365b28d80c5bf
7
- data.tar.gz: 854a1cc894da5a8706e084f1b2e328a3529470f785e4cc22c27d80080ec7c2780cf3db40eb3f919fd9afd359539a879af82d241b6a2061f3673fa80ead876357
6
+ metadata.gz: f5a369e373c45e9d4343666eb59e2adbf16a1620efe57052b89f655be7d09bc63c30d120c7a47dc873e5ed0ac1f6af5fb2b23c4b71847819ac15c9f3c2e0f5a3
7
+ data.tar.gz: 51ac21d3ace42a9ebefaa03ee2fdf924a6dd9549eb41d053901f0a2c6d115147e35a9f90e77438241937ca2171fdbf6a295792bfbef4528e209b5211f1d598b4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
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
+
3
11
  ## [0.5.1] - 2026-09-26
4
12
 
5
13
  ### Documentation
data/Rakefile CHANGED
@@ -9,76 +9,35 @@ require "rubocop/rake_task"
9
9
 
10
10
  RuboCop::RakeTask.new
11
11
 
12
- # Files included in llms-full.txt, in reading order. Keep README first, then
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
- File.write(LLMS_FULL_PATH, build_llms_full)
70
- puts "Wrote #{LLMS_FULL_PATH} (#{File.size(LLMS_FULL_PATH)} bytes, #{LLMS_SOURCE_FILES.size} source files)"
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 is up to date with README and docs/"
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
- expected = build_llms_full
76
- actual = File.exist?(LLMS_FULL_PATH) ? File.read(LLMS_FULL_PATH, encoding: "UTF-8") : ""
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 expected == actual
79
- puts "✓ #{LLMS_FULL_PATH} is up to date"
37
+ if stale.empty?
38
+ puts "✓ llms.txt and llms-full.txt are up to date"
80
39
  else
81
- warn "✗ #{LLMS_FULL_PATH} is out of date with README/docs."
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 llms-full.txt (alias for llms:build)"
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
@@ -68,6 +68,18 @@ Five things to know:
68
68
  the envelope before building a resource, so there's nothing left of it by
69
69
  the time `raw` is populated. If you need the envelope back, you're
70
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.
71
83
  - **It's the JSON representation.** Kiwi sends
72
84
  `Accept: application/json`. Xero also serves XML, which nests
73
85
  differently — XML has no arrays, so a single child parses to a Hash and
@@ -78,14 +90,46 @@ Five things to know:
78
90
  the modelled attributes — different keys, different nesting. If you store
79
91
  `to_h` where you meant to store the payload, readers fail by returning
80
92
  nil rather than raising.
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.
85
93
  - **It costs memory.** Every resource holds its source hash alongside the
86
94
  hydrated attributes, which roughly doubles the footprint of a large page.
87
95
  That's why it's off by default.
88
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
+
89
133
  ### Migrating from an XML-based client
90
134
 
91
135
  If you're replacing a Xero client that sent `Accept: text/xml` — HTTParty
@@ -139,11 +139,23 @@ module XeroKiwi
139
139
  # arrived, or nil unless the client was built with `retain_raw: true`.
140
140
  # Use it to reach fields the gem doesn't model.
141
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
+ #
142
149
  # Scope is the item, NOT the enclosing envelope: `contact.raw` has no
143
150
  # "Contacts" key, and `organisation.raw` no "Organisations" key. Kiwi
144
151
  # unwraps the envelope before a resource is built, so there is nothing
145
152
  # left of it by the time this is populated.
146
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
+ #
147
159
  # It is also the JSON representation specifically. Kiwi sends
148
160
  # `Accept: application/json`; Xero's XML representation nests
149
161
  # differently — no arrays, so one child is a Hash and several are an
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module XeroKiwi
4
- VERSION = "0.5.1"
4
+ VERSION = "0.5.2"
5
5
  end