basecradle 0.8.0 → 0.10.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +104 -0
- data/README.md +80 -5
- data/lib/basecradle/api_object.rb +42 -0
- data/lib/basecradle/timeline.rb +48 -2
- data/lib/basecradle/version.rb +1 -1
- data/lib/basecradle/webhooks.rb +7 -5
- 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: 3acdf9b8dad4d29a40926030c480cc415b96ffff03c56a121838b4fdb42cd638
|
|
4
|
+
data.tar.gz: 2b88488a27cae407aa366cce9899da840b7ea1e74db86d21c511f0f2b1ac47d7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7f753c98128c1935e0503dd1963917ac1918e171401b4a39f6c1b650cd716213f02c52b1dd538a54bb201de0ede89af19e7ab2ec9abfa79d2eba68d71faa5f57
|
|
7
|
+
data.tar.gz: 31772a4161d2e3563a835ffbd2c9ae5a2d9e285a157ca2772be3213037297b168f8e89b67e8290a7897a77a009add74b9769bf5e623ac5256ec19f26eece04d9
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,108 @@ All notable changes to this project are documented here. The format is based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
One deliberate departure from that format: this changelog keeps **no `Unreleased`
|
|
8
|
+
section**. The newest heading is always the version `lib/basecradle/version.rb` builds —
|
|
9
|
+
a release writes its entry and its version in the same PR — and `test/changelog_test.rb`
|
|
10
|
+
fails CI if the two ever disagree.
|
|
11
|
+
|
|
12
|
+
## [0.10.0] - 2026-09-30
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **A model serializes as the record it stands for**
|
|
17
|
+
([#191](https://github.com/basecradle/basecradle-ruby/issues/191)). `ApiObject` defined
|
|
18
|
+
no `to_json`, so serializing *any* model — `bc.me`, a `Timeline`, a `Message`, any
|
|
19
|
+
content object — fell through to Ruby's default and emitted a heap address:
|
|
20
|
+
`"#<BaseCradle::MessageContent:0x000072be94b09…>"`. Silently, with no field names and a
|
|
21
|
+
different value every run. `model.to_json` and `JSON.generate(model)` now emit the
|
|
22
|
+
record, and so does a model nested in an array or a hash. What `to_h` holds is exactly
|
|
23
|
+
what goes out: nothing renamed, nothing dropped, and a field newer than this SDK
|
|
24
|
+
release serialized like any other.
|
|
25
|
+
`WebhookEventHeaders` is unchanged — it subclasses `Hash`, so it already serialized as
|
|
26
|
+
the delivery's headers.
|
|
27
|
+
Long-standing; 0.9.0's typing of `timeline.items` content only widened which read path
|
|
28
|
+
reached it. **0.9.0's Migrating note on `to_json` is superseded by this release.**
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **`as_json`**, ActiveSupport's serialization hook, returning the same wire `Hash` as
|
|
33
|
+
`to_h`. It is what reaches a model nested inside a structure Rails renders
|
|
34
|
+
(`render json: { user: bc.me }`); a bare `render json: model` calls `to_json` directly
|
|
35
|
+
and never reaches it. Two things to know, neither of which a Rails habit expects:
|
|
36
|
+
- **It returns the same `Hash` as `to_h`, not a copy** — read-only by the same
|
|
37
|
+
convention, where every ActiveSupport `as_json` builds a fresh one. So
|
|
38
|
+
`model.as_json.merge!(extra)` rewrites the model's wire record, and for a wrapped
|
|
39
|
+
child its parent's too. Use `model.to_h.dup` (shallow) if you need to touch it.
|
|
40
|
+
- **Options are accepted and ignored, `only:` and `except:` included** — a model is a
|
|
41
|
+
read of one record, not a presenter. `as_json(except: ["ingest_url"])` returns the
|
|
42
|
+
whole record, silently, and it *disagrees* with `to_json`: with ActiveSupport loaded
|
|
43
|
+
`model.to_json(except: [...])` does redact, because the option reaches `Hash#to_json`.
|
|
44
|
+
Do not route redaction through either — build the subset with
|
|
45
|
+
`model.to_h.except("ingest_url")`.
|
|
46
|
+
|
|
47
|
+
`to_json` forwards its argument to `Hash#to_json` rather than swallowing it, so
|
|
48
|
+
`JSON.pretty_generate` pretty-prints; what an unknown option does is the host app's
|
|
49
|
+
`json` version's business (3.x raises, the 2.x older Rubies ship ignores it).
|
|
50
|
+
|
|
51
|
+
`to_s` is untouched, so interpolating a model (`"#{model}"`, `puts model`) still shows
|
|
52
|
+
the default object form. `inspect` names the model and its fields; `to_json` / `to_h`
|
|
53
|
+
give the record.
|
|
54
|
+
|
|
55
|
+
Minor rather than patch: the `to_json` output is a fix, but `as_json` is new public
|
|
56
|
+
surface and 0.9.0 documented the old behavior as expected — semver takes the higher.
|
|
57
|
+
|
|
58
|
+
## [0.9.0] - 2026-09-30
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
|
|
62
|
+
- **A timeline item's `content` is typed by the item's `type`**
|
|
63
|
+
([#189](https://github.com/basecradle/basecradle-ruby/issues/189)). `item.content` now
|
|
64
|
+
reads as the same content model the record's own resource returns — `MessageContent`,
|
|
65
|
+
`AssetContent`, `TaskContent` or `WebhookEventContent` — where it was the raw wire
|
|
66
|
+
`Hash`. One record has one shape however you reach it, so everything those models add
|
|
67
|
+
reads down both paths: `timeline.items` gives an `AssetContent` whose `file` is an
|
|
68
|
+
`AssetFile`, and a `webhook_event` row's `content.headers` is the case-folding
|
|
69
|
+
`WebhookEventHeaders` 0.8.0 shipped, so `item.content.headers["X-GitHub-Delivery"]`
|
|
70
|
+
now finds the header it names. The previous release documented that split as a
|
|
71
|
+
boundary; it is now closed, and every future enrichment of a content model arrives on
|
|
72
|
+
both paths at once. The README documents `timeline.items` with a worked example for
|
|
73
|
+
the first time.
|
|
74
|
+
- **An item `type` this release does not know keeps reading.** The API is additive-only,
|
|
75
|
+
so an unrecognized `type` is never an error: its content comes back as a plain
|
|
76
|
+
`ApiObject`, and `content["any_field"]` still returns the wire value untouched.
|
|
77
|
+
Upgrading the SDK is what types it. An item carrying *no* `type` is a different thing
|
|
78
|
+
— a malformed response, not a new record kind — and raises `MissingFieldError` the way
|
|
79
|
+
`item.type` itself does, rather than being guessed into the generic case.
|
|
80
|
+
- The wire is untouched — `item["content"]`, `ApiObject`'s raw escape hatch, still hands
|
|
81
|
+
back exactly the `Hash` the API sent, and no field is renamed.
|
|
82
|
+
Decided in lockstep with the Python SDK's
|
|
83
|
+
[#210](https://github.com/basecradle/basecradle-python/issues/210); that port is still
|
|
84
|
+
in flight, so the two SDKs reach parity on this when it ships.
|
|
85
|
+
|
|
86
|
+
### Migrating
|
|
87
|
+
|
|
88
|
+
`item.content` was a `Hash` and is now an `ApiObject`. Reads of *named* fields get
|
|
89
|
+
better — `item.content.body` instead of `item.content["body"]`, and the models' own
|
|
90
|
+
enrichments come with them — but three things that worked on a `Hash` no longer do:
|
|
91
|
+
|
|
92
|
+
- **The `Hash` protocol is gone.** `item.content.fetch("body")`, `.each`, `.keys`,
|
|
93
|
+
`.dig(...)` and `.key?(...)` raise `NoMethodError`. Use the field reader, or
|
|
94
|
+
`item.content.to_h` for the wire `Hash` itself (`item["content"]` gives the same).
|
|
95
|
+
- **`to_json` no longer serializes the record.** `item.content.to_json` emits Ruby's
|
|
96
|
+
default `to_s` — `"#<BaseCradle::MessageContent:0x…>"`, an object address with no
|
|
97
|
+
field names — because `ApiObject` defines no `to_json`. That is long-standing for
|
|
98
|
+
every other model in the SDK and now reaches this path too. Serialize
|
|
99
|
+
`item.content.to_h` instead.
|
|
100
|
+
> ⚠️ **Superseded in [0.10.0](#0100---2026-09-30)**, which gives `ApiObject` a
|
|
101
|
+
> `to_json`. `item.content.to_json` emits the record; the `to_h` workaround above is
|
|
102
|
+
> no longer needed. Left as written for the historical record.
|
|
103
|
+
- **`inspect` and `==` change, both for the better.** `inspect` names the model and its
|
|
104
|
+
wire fields (`#<BaseCradle::MessageContent body, uuid>`) rather than printing a `Hash`;
|
|
105
|
+
and equality now *holds* between an item's content and the same record fetched
|
|
106
|
+
directly (`timeline.items.first.content == bc.messages.get(uuid).content`), where
|
|
107
|
+
comparing a `Hash` to an `ApiObject` was always false.
|
|
108
|
+
|
|
7
109
|
## [0.8.0] - 2026-09-30
|
|
8
110
|
|
|
9
111
|
### Changed
|
|
@@ -294,6 +396,8 @@ the Python SDK's behavior in idiomatic Ruby. Zero runtime dependencies.
|
|
|
294
396
|
- **Quality bars** — a README-as-tested-doc harness (every example runs against a mocked
|
|
295
397
|
API) and a spec drift-guard (CI fails if the live API grows beyond the SDK).
|
|
296
398
|
|
|
399
|
+
[0.10.0]: https://github.com/basecradle/basecradle-ruby/releases/tag/v0.10.0
|
|
400
|
+
[0.9.0]: https://github.com/basecradle/basecradle-ruby/releases/tag/v0.9.0
|
|
297
401
|
[0.8.0]: https://github.com/basecradle/basecradle-ruby/releases/tag/v0.8.0
|
|
298
402
|
[0.7.0]: https://github.com/basecradle/basecradle-ruby/releases/tag/v0.7.0
|
|
299
403
|
[0.6.1]: https://github.com/basecradle/basecradle-ruby/releases/tag/v0.6.1
|
data/README.md
CHANGED
|
@@ -128,6 +128,39 @@ end
|
|
|
128
128
|
|
|
129
129
|
`cancel` withdraws a still-*pending* task — the scheduled-work equivalent of `timeline.lock`: its alarm never fires and the slot it held under your `max_pending_tasks` cap is freed at once. It is author-only (an admin may cancel any task) and works even on a locked timeline (cancellation is cleanup, not new content). Cancelling a task you did not author raises `BaseCradle::NotTaskAuthorError`; cancelling one that is no longer pending — already activated, blocked, or cancelled — raises `BaseCradle::TaskNotPendingError`. Create-then-cancel-and-reschedule gives you a rolling **dead man's switch**: a task that fires only if you stop renewing it.
|
|
130
130
|
|
|
131
|
+
## Reading a timeline's items
|
|
132
|
+
|
|
133
|
+
Fetching one timeline gives you everything on it inline — a single mixed list, **oldest
|
|
134
|
+
first** (it is the timeline as read, not a paginated feed). `item.type` says which record
|
|
135
|
+
each row is, and `item.content` is that record's own content model — the very object the
|
|
136
|
+
record's own resource returns:
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
require "basecradle"
|
|
140
|
+
|
|
141
|
+
bc = BaseCradle::Client.new
|
|
142
|
+
|
|
143
|
+
bc.timelines.get("019e7750-66ee-7f53-829f-13a8a710b6da").items.each do |item|
|
|
144
|
+
case item.type
|
|
145
|
+
when "message" then puts item.content.body
|
|
146
|
+
when "asset" then puts item.content.file.filename
|
|
147
|
+
when "task" then puts item.content.status
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Two things the list does not carry. A **pending** task is not on it — a task appears only
|
|
153
|
+
once it activates — so survey scheduled work with `bc.tasks.filter(status: "pending")`
|
|
154
|
+
instead. And an item `type` this SDK release does not know is not an error: the API is
|
|
155
|
+
additive-only, so its content reads as a plain `BaseCradle::ApiObject` and
|
|
156
|
+
`content["any_field"]` still returns the wire value untouched.
|
|
157
|
+
|
|
158
|
+
Two fields are type-specific, so branch on `type` before reaching for them: a
|
|
159
|
+
`webhook_event` item has no author (it came from an external sender, not a peer), and it
|
|
160
|
+
alone carries `webhook_endpoint`, the endpoint it arrived on, embedded in full. Reading
|
|
161
|
+
either where it does not belong raises `BaseCradle::MissingFieldError` rather than
|
|
162
|
+
inventing a value.
|
|
163
|
+
|
|
131
164
|
## Webhooks
|
|
132
165
|
|
|
133
166
|
External services deliver into a timeline by POSTing to an endpoint's secret ingest URL. Each delivery becomes a readable event. This is the **inbound** direction — data arriving at BaseCradle. Its outbound counterpart is Event Delivery, the platform's push through your integration, which the SDK does not model.
|
|
@@ -187,11 +220,12 @@ fallback-less `fetch` raise `KeyError` naming the headers that did arrive, while
|
|
|
187
220
|
and the methods that reshape it (`slice`, `except`, `select`) work on those stored names,
|
|
188
221
|
case-sensitively.
|
|
189
222
|
|
|
190
|
-
|
|
191
|
-
`
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
223
|
+
This holds however you reach the record. A timeline item's `content` is typed by the
|
|
224
|
+
item's own `type`, so a `webhook_event` row of `timeline.items` is a `WebhookEventContent`
|
|
225
|
+
just like one from `bc.webhook_events` — `item.content.headers["X-GitHub-Delivery"]` folds
|
|
226
|
+
the same way. What gives the folding up is converting away from the type: both
|
|
227
|
+
`event.content["headers"]` (`ApiObject`'s raw-wire escape hatch, which wraps nothing) and
|
|
228
|
+
`headers.to_h` hand back a plain, case-sensitive `Hash`.
|
|
195
229
|
|
|
196
230
|
## Idempotent creates & safe retries
|
|
197
231
|
|
|
@@ -290,6 +324,41 @@ timeline = bc.timelines.create(name: "Incident response")
|
|
|
290
324
|
timeline.add_participant(nova)
|
|
291
325
|
```
|
|
292
326
|
|
|
327
|
+
## Reading models, and serializing them
|
|
328
|
+
|
|
329
|
+
Every model is a read-only, wire-exact view of one API JSON object. Field readers mirror
|
|
330
|
+
the wire names; `model[key]` is the raw escape hatch for a field newer than your SDK
|
|
331
|
+
release; `model.to_h` is the wire `Hash` itself.
|
|
332
|
+
|
|
333
|
+
Models serialize as the record they stand for — what `to_h` holds is what goes out, so
|
|
334
|
+
logging or re-emitting one gives you the wire record rather than an object address:
|
|
335
|
+
|
|
336
|
+
```ruby
|
|
337
|
+
require "basecradle"
|
|
338
|
+
require "json"
|
|
339
|
+
|
|
340
|
+
bc = BaseCradle::Client.new
|
|
341
|
+
message = bc.messages.first
|
|
342
|
+
|
|
343
|
+
puts message.content.to_json # {"uuid":"019e...","body":"Hello from a peer."}
|
|
344
|
+
puts JSON.generate([message.content]) # nested in an array
|
|
345
|
+
puts({ latest: message.content }.to_json) # nested in a hash
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
`as_json` is ActiveSupport's hook and returns the same wire `Hash`, so a model nested in
|
|
349
|
+
something Rails renders comes out right too. Three caveats worth knowing:
|
|
350
|
+
|
|
351
|
+
- **`as_json` returns the same `Hash` as `to_h`, not a copy** — read-only by the same
|
|
352
|
+
convention. Mutating it rewrites the model's record. Use `model.to_h.dup` (shallow).
|
|
353
|
+
- **Options are accepted and ignored, `only:` and `except:` included.** A model is a read
|
|
354
|
+
of one record, not a presenter, so `as_json(except: ["ingest_url"])` returns the whole
|
|
355
|
+
record, silently — and it *disagrees* with `to_json`, which under ActiveSupport does
|
|
356
|
+
redact, because the option reaches `Hash#to_json`. Do not route redaction through
|
|
357
|
+
either: build the subset with `model.to_h.except("ingest_url")`.
|
|
358
|
+
- **`to_json` forwards its argument** to `Hash#to_json`, so `JSON.pretty_generate`
|
|
359
|
+
pretty-prints. What an unknown option does is your `json` version's business — 3.x
|
|
360
|
+
raises, the 2.x older Rubies ship ignores it.
|
|
361
|
+
|
|
293
362
|
## Development
|
|
294
363
|
|
|
295
364
|
```bash
|
|
@@ -298,8 +367,14 @@ bundle exec rake # lint + tests (offline — the default)
|
|
|
298
367
|
bundle exec rake test:live # the spec drift-guard (one network call to the live spec)
|
|
299
368
|
bundle exec rubocop # lint only
|
|
300
369
|
gem build basecradle.gemspec # build the gem
|
|
370
|
+
actionlint # lint the GitHub workflows (CI runs this; see below)
|
|
301
371
|
```
|
|
302
372
|
|
|
373
|
+
CI additionally lints the workflow files with [actionlint](https://github.com/rhysd/actionlint)
|
|
374
|
+
(which runs `shellcheck` over each `run:` block). To reproduce that check locally, install
|
|
375
|
+
both and run `actionlint` from the repo root — CI pins actionlint by version and SHA256 in
|
|
376
|
+
`.github/workflows/ci.yml`; match that version to see what CI sees.
|
|
377
|
+
|
|
303
378
|
## Contributing
|
|
304
379
|
|
|
305
380
|
Human and AI contributors work under identical rules here: branch → PR → green CI → merge. See [`CLAUDE.md`](CLAUDE.md) for the project conventions and the issues for the roadmap.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
3
5
|
require_relative "errors"
|
|
4
6
|
|
|
5
7
|
module BaseCradle
|
|
@@ -61,6 +63,46 @@ module BaseCradle
|
|
|
61
63
|
@data
|
|
62
64
|
end
|
|
63
65
|
|
|
66
|
+
# The wire record, for a serializer — ActiveSupport's hook, and what reaches a model
|
|
67
|
+
# nested inside a structure Rails renders (<tt>render json: { user: bc.me }</tt>).
|
|
68
|
+
# A bare <tt>render json: model</tt> calls +to_json+ instead and never comes here.
|
|
69
|
+
#
|
|
70
|
+
# Returns +to_h+: the wire keys, nothing renamed, nothing dropped, and a field newer
|
|
71
|
+
# than this release carried like any other. (It is the record *as this object holds
|
|
72
|
+
# it* — for a Timeline fetched whole, that is the API's two-key envelope merged into
|
|
73
|
+
# one object, so it carries +items+ alongside the timeline's own fields.)
|
|
74
|
+
#
|
|
75
|
+
# Two traps, because neither is what a Rails habit expects:
|
|
76
|
+
#
|
|
77
|
+
# - **It is the same Hash +to_h+ returns, not a copy** — read-only by the same
|
|
78
|
+
# convention, where every ActiveSupport +as_json+ builds a fresh one. So
|
|
79
|
+
# <tt>model.as_json.merge!(extra)</tt> rewrites the model's wire record, and for a
|
|
80
|
+
# wrapped child its parent's too (a child wraps the parent's own nested Hash).
|
|
81
|
+
# +to_h.dup+ if you need to touch it; note that is shallow.
|
|
82
|
+
# - **Options are accepted and ignored, +only:+ and +except:+ included** — a model is
|
|
83
|
+
# a read of one record, not a presenter. +as_json(except: ["ingest_url"])+ returns
|
|
84
|
+
# the whole record, *silently*. Worse, it disagrees with +to_json+: with
|
|
85
|
+
# ActiveSupport loaded <tt>model.to_json(except: [...])</tt> *does* redact, because
|
|
86
|
+
# the option reaches +Hash#to_json+. Do not rely on either — build the subset
|
|
87
|
+
# yourself with <tt>model.to_h.except("ingest_url")</tt>.
|
|
88
|
+
def as_json(*)
|
|
89
|
+
to_h
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# The wire record as JSON. Without this a model fell through to Ruby's default and
|
|
93
|
+
# emitted a heap address — silently, with no field names and a different value every
|
|
94
|
+
# run. +JSON.generate+ and +Array#to_json+ / +Hash#to_json+ call this, so a model
|
|
95
|
+
# nested in either serializes correctly too.
|
|
96
|
+
#
|
|
97
|
+
# The argument is forwarded to +Hash#to_json+ rather than swallowed, so
|
|
98
|
+
# +JSON.pretty_generate+ pretty-prints — and what an unknown option does is the host
|
|
99
|
+
# app's json version's business, not this SDK's: json 3.x raises on an unknown
|
|
100
|
+
# keyword, the json 2.x that older Rubies ship ignores it, and ActiveSupport acts on
|
|
101
|
+
# +only:+ / +except:+. See +as_json+ above: do not route redaction through either.
|
|
102
|
+
def to_json(*args)
|
|
103
|
+
to_h.to_json(*args)
|
|
104
|
+
end
|
|
105
|
+
|
|
64
106
|
def ==(other)
|
|
65
107
|
other.instance_of?(self.class) && other.to_h == @data
|
|
66
108
|
end
|
data/lib/basecradle/timeline.rb
CHANGED
|
@@ -7,7 +7,7 @@ require_relative "webhooks"
|
|
|
7
7
|
|
|
8
8
|
module BaseCradle
|
|
9
9
|
# One item on a timeline — a message, asset, webhook event, or task. +type+ says which;
|
|
10
|
-
# +content+ is the item itself,
|
|
10
|
+
# +content+ is the item itself, typed by that +type+; +user+ is the author.
|
|
11
11
|
#
|
|
12
12
|
# An inline item is the record's own standalone form, with one difference: +created_at+
|
|
13
13
|
# is the *item's* — when the record landed on the timeline (for a task, its activation;
|
|
@@ -19,13 +19,59 @@ module BaseCradle
|
|
|
19
19
|
# Reading either where it does not belong raises +MissingFieldError+ rather than
|
|
20
20
|
# inventing a value.
|
|
21
21
|
class TimelineItem < ApiObject
|
|
22
|
+
# The content model each item +type+ reads as — the very class that +type+'s own
|
|
23
|
+
# resource returns, so one record has one shape however you reach it. A +type+ this
|
|
24
|
+
# release does not know is not in here on purpose; see +content+. Internal: the
|
|
25
|
+
# dispatch table is not API surface, so re-keying it is never a breaking change.
|
|
26
|
+
CONTENT_MODELS = {
|
|
27
|
+
"message" => MessageContent,
|
|
28
|
+
"asset" => AssetContent,
|
|
29
|
+
"task" => TaskContent,
|
|
30
|
+
"webhook_event" => WebhookEventContent
|
|
31
|
+
}.freeze
|
|
32
|
+
private_constant :CONTENT_MODELS
|
|
33
|
+
|
|
22
34
|
attribute :type
|
|
23
35
|
attribute :created_at
|
|
24
36
|
attribute :updated_at
|
|
25
37
|
attribute :user, wrap: User
|
|
26
38
|
attribute :timeline, wrap: Reference
|
|
27
39
|
attribute :webhook_endpoint, wrap: WebhookEndpoint # webhook_event items only
|
|
28
|
-
|
|
40
|
+
|
|
41
|
+
# The item itself, as the content model its +type+ names: a MessageContent,
|
|
42
|
+
# AssetContent, TaskContent or WebhookEventContent — the same object the record's own
|
|
43
|
+
# page hands back. So a record reads identically down either path, enrichments
|
|
44
|
+
# included (+item.content.headers+ folds case exactly as
|
|
45
|
+
# +bc.webhook_events.get(uuid).content.headers+ does), and an item's content +==+ the
|
|
46
|
+
# same record fetched directly.
|
|
47
|
+
#
|
|
48
|
+
# The API is additive-only, so an item +type+ newer than this SDK release must keep
|
|
49
|
+
# reading rather than raise: its content comes back as a plain ApiObject, whose +[]+
|
|
50
|
+
# still returns every wire field untouched. Upgrading the SDK is what types it.
|
|
51
|
+
def content
|
|
52
|
+
raise_missing("content") unless to_h.key?("content")
|
|
53
|
+
# The two ways a +type+ can be unusable are different answers, not one. A type the
|
|
54
|
+
# API sent that this release has no model for is the forward-compatible case and
|
|
55
|
+
# falls back below; an item carrying no +type+ at all is a malformed response (the
|
|
56
|
+
# spec marks it required), and the SDK raises rather than guessing which — the same
|
|
57
|
+
# promise ApiObject makes for every other withheld field.
|
|
58
|
+
raise_untypable unless to_h.key?("type")
|
|
59
|
+
|
|
60
|
+
wrap_value(to_h["content"], CONTENT_MODELS.fetch(type, ApiObject))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
# Reading +type+ here would raise about a field the caller never asked for, on an
|
|
66
|
+
# object whose "fields present" list includes the +content+ they did ask for. Name
|
|
67
|
+
# the dependency between the two instead, and the escape hatch that still works.
|
|
68
|
+
def raise_untypable
|
|
69
|
+
raise MissingFieldError,
|
|
70
|
+
"This #{self.class}'s content cannot be typed: the API did not return " \
|
|
71
|
+
"\"type\" for it, and an item's content model is chosen by its type. The " \
|
|
72
|
+
"wire content is still readable with item[\"content\"]. " \
|
|
73
|
+
"Fields present: #{to_h.keys.sort.inspect}"
|
|
74
|
+
end
|
|
29
75
|
end
|
|
30
76
|
|
|
31
77
|
# A timeline: its metadata, owner, participants, lock state — and its verbs.
|
data/lib/basecradle/version.rb
CHANGED
data/lib/basecradle/webhooks.rb
CHANGED
|
@@ -196,11 +196,13 @@ module BaseCradle
|
|
|
196
196
|
# two are historical, and they are the only ones.
|
|
197
197
|
#
|
|
198
198
|
# +headers+ is the delivery's request headers as a +WebhookEventHeaders+ — the wire's
|
|
199
|
-
# own pairs, looked up case-insensitively.
|
|
200
|
-
#
|
|
201
|
-
#
|
|
202
|
-
#
|
|
203
|
-
#
|
|
199
|
+
# own pairs, looked up case-insensitively. This is the content model every read of the
|
|
200
|
+
# record returns, +timeline.items+ included: a +webhook_event+ row's +content+ is one of
|
|
201
|
+
# these (TimelineItem types content by the item's +type+), so the folding lookup is
|
|
202
|
+
# there too. What gives it up is converting away from the type — +content["headers"]+,
|
|
203
|
+
# ApiObject's raw-wire escape hatch, which wraps nothing, and the conversions
|
|
204
|
+
# WebhookEventHeaders names above (+to_h+ and friends) all hand back a plain,
|
|
205
|
+
# case-sensitive Hash.
|
|
204
206
|
class WebhookEventContent < ApiObject
|
|
205
207
|
attribute :uuid
|
|
206
208
|
attribute :content_type
|