Skip to content

Commit 684ad2b

Browse files
Fix/circular reference rendering (#10)
* Add cyclic examples * FIx the recursive cyclic bug * Refactor code
1 parent 149eb2a commit 684ad2b

16 files changed

Lines changed: 907 additions & 140 deletions

examples/bookstore/README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ The BookStore service exercises every proto3 feature gRPC Studio supports:
1111
| Feature | Where |
1212
|---------|-------|
1313
| Deep nesting (3 levels) | `Book``Publisher``Address``Coordinates` |
14+
| Recursive (direct) | `Book.prequel` — a `Book` inside a `Book` |
15+
| Recursive (repeated) | `Book.sequels``repeated Book` |
16+
| Recursive (indirect) | `Book.lineage``BookLineage.predecessor``Book`, plus `BookLineage.branches` |
1417
| Enums | `Genre`, `Availability` |
1518
| Repeated scalar | `alternate_titles` |
1619
| Repeated message | `reviews`, `editions` |
@@ -60,16 +63,25 @@ The server starts with 3 pre-loaded books:
6063

6164
| ID | Title | Genre | Availability | Highlights |
6265
|----|-------|-------|--------------|------------|
63-
| book-001 | Dune | Science | In stock | Full publisher, reviews, editions, map tags, struct metadata |
66+
| book-001 | Harry Potter and the Philosopher's Stone | Fantasy | In stock | Full publisher, reviews, editions, map tags, struct metadata, **recursive relatives** |
6467
| book-002 | The Hobbit | Fantasy | Preorder | Series info (oneOf), alternate titles, formats in metadata |
6568
| book-003 | Goodnight Moon | Children | In stock | Internal SKU (oneOf), no publisher, minimal records |
6669

70+
book-001 carries all three cycle shapes: a `prequel` (Fantastic Beasts), two
71+
`sequels` (Chamber of Secrets, Goblet of Fire — Chamber of Secrets in turn has
72+
its own `sequels`, so the payload is three `Book` levels deep), and a `lineage`
73+
chain with recursive `branches`. The related volumes are reachable only through
74+
those fields; they are not separate `ListBooks` records.
75+
6776
## RPCs to Try
6877

6978
- **ListBooks** — returns all books, supports `genre_filter` and `availability_filter`
70-
- **GetBook**`{ "id": "book-001" }` to see Dune with all nested types
79+
- **GetBook**`{ "id": "book-001" }` to see the Philosopher's Stone with all nested types
7180
- **CreateBook** — create a new book with any combination of fields
7281
- **SearchBooks** — text search with `query`, filter by `genres` array, `max_page_count`, `max_price_usd`
82+
- **GetBookSeries**`{ "id": "book-001", "depth": 4 }` returns a `Book` whose
83+
`prequel`, `sequels` and `lineage` nest to the requested depth. Use this to
84+
stress a renderer against a cyclic schema; `depth` is capped at 10
7385
- **WatchBooks** — server stream that emits random catalog events every 2 seconds
7486
- **BulkCreateBooks** — send multiple books as a client stream
7587
- **CheckStock** — send `{ "book_id": "book-001", "warehouses": ["east", "west"] }` and get quantities back

examples/bookstore/proto/bookstore.proto

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,26 @@ message Edition {
100100
bool signed_copy = 7;
101101
}
102102

103+
// ---------------------------------------------------------------------------
104+
// Recursive / cyclic message types
105+
//
106+
// The schema graph below contains cycles: Book → Book (direct), Book →
107+
// repeated Book, and Book → BookLineage → Book (indirect). BookLineage is
108+
// also recursive through its own type. Any renderer that walks the descriptor
109+
// eagerly will recurse forever here, so these fields exercise cycle detection
110+
// in both request forms (CreateBook takes a Book) and response views (GetBook
111+
// returns a Book).
112+
// ---------------------------------------------------------------------------
113+
114+
message BookLineage {
115+
string catalog_id = 1;
116+
// Indirect cycle: BookLineage → Book → BookLineage
117+
Book predecessor = 2;
118+
// Self-recursive: BookLineage → BookLineage
119+
repeated BookLineage branches = 3;
120+
int32 generation = 4;
121+
}
122+
103123
// ---------------------------------------------------------------------------
104124
// Main Book message — exercises every proto3 feature
105125
// ---------------------------------------------------------------------------
@@ -147,6 +167,15 @@ message Book {
147167

148168
// Any — extensible typed payload (e.g. for event-specific data)
149169
google.protobuf.Any extra_info = 22;
170+
171+
// Direct self-reference — Book contains a Book
172+
Book prequel = 23;
173+
174+
// Repeated self-reference — Book contains many Books
175+
repeated Book sequels = 24;
176+
177+
// Indirect self-reference — Book → BookLineage → Book
178+
BookLineage lineage = 25;
150179
}
151180

152181
// ---------------------------------------------------------------------------
@@ -210,6 +239,14 @@ message BulkCreateBooksResponse {
210239
repeated string errors = 3;
211240
}
212241

242+
// Recursive messages
243+
244+
message GetBookSeriesRequest {
245+
string id = 1;
246+
// How many prequel/sequel levels to populate (default 3, max 10).
247+
int32 depth = 2;
248+
}
249+
213250
message StockCheckRequest {
214251
string book_id = 1;
215252
repeated string warehouses = 2;
@@ -235,6 +272,9 @@ service BookStoreService {
235272
rpc ListBooks(ListBooksRequest) returns (ListBooksResponse);
236273
rpc SearchBooks(SearchBooksRequest) returns (ListBooksResponse);
237274

275+
// Recursive payloads — response nests Book inside Book several levels deep
276+
rpc GetBookSeries(GetBookSeriesRequest) returns (Book);
277+
238278
// Server streaming — live catalog event feed
239279
rpc WatchBooks(WatchBooksRequest) returns (stream BookEvent);
240280

468 Bytes
Binary file not shown.

examples/bookstore/src/handlers.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,77 @@ function SearchBooks(call, callback) {
134134
callback(null, { books: results, next_page_token: '', total_count: results.length });
135135
}
136136

137+
// ---------------------------------------------------------------------------
138+
// Recursive payload — Book nested inside Book
139+
//
140+
// Builds a series chain `depth` levels deep using the self-referencing fields
141+
// (prequel / sequels / lineage). Useful for exercising renderers against
142+
// cyclic schemas: the descriptor cycle is infinite, but the payload is not.
143+
// ---------------------------------------------------------------------------
144+
145+
const MAX_SERIES_DEPTH = 10;
146+
const DEFAULT_SERIES_DEPTH = 3;
147+
148+
// Strip self-referencing fields so a seeded book can be used as a chain node
149+
// without dragging its own (already-populated) relatives along.
150+
function withoutRelatives(book) {
151+
const { prequel: _prequel, sequels: _sequels, lineage: _lineage, ...rest } = book;
152+
return rest;
153+
}
154+
155+
function buildSeries(book, depth, generation) {
156+
const node = withoutRelatives(book);
157+
if (depth <= 0) return node;
158+
159+
const predecessor = buildSeries(book, depth - 1, generation + 1);
160+
161+
return {
162+
...node,
163+
// Direct self-reference
164+
prequel: {
165+
...predecessor,
166+
id: `${book.id}-prequel${generation}`,
167+
title: `${book.title}: Book ${generation}`,
168+
},
169+
// Repeated self-reference
170+
sequels: [
171+
{
172+
...buildSeries(book, depth - 1, generation + 1),
173+
id: `${book.id}-sequel${generation}a`,
174+
title: `${book.title}: Book ${generation + 1}`,
175+
},
176+
{
177+
...withoutRelatives(book),
178+
id: `${book.id}-sequel${generation}b`,
179+
title: `${book.title}: Companion Volume`,
180+
},
181+
],
182+
// Indirect self-reference — Book → BookLineage → Book
183+
lineage: {
184+
catalog_id: `cat-${book.id}-${generation}`,
185+
generation,
186+
predecessor,
187+
branches: [
188+
{
189+
catalog_id: `cat-${book.id}-${generation}-a`,
190+
generation: generation + 1,
191+
branches: [],
192+
},
193+
],
194+
},
195+
};
196+
}
197+
198+
function GetBookSeries(call, callback) {
199+
const book = store.get(call.request.id);
200+
if (!book) return notFound(call, callback, call.request.id);
201+
202+
const requested = call.request.depth || DEFAULT_SERIES_DEPTH;
203+
const depth = Math.min(Math.max(requested, 0), MAX_SERIES_DEPTH);
204+
205+
callback(null, buildSeries(book, depth, 1));
206+
}
207+
137208
// ---------------------------------------------------------------------------
138209
// Server streaming — live catalog event feed
139210
// ---------------------------------------------------------------------------
@@ -258,6 +329,7 @@ module.exports = {
258329
DeleteBook,
259330
ListBooks,
260331
SearchBooks,
332+
GetBookSeries,
261333
WatchBooks,
262334
BulkCreateBooks,
263335
CheckStock,

0 commit comments

Comments
 (0)