Skip to content

Commit 4dc5f71

Browse files
authored
Merge pull request #121 from speakeasy-api/walker/go-sdk-render-accessors
Replace Guide.Render with RenderExternal and RenderSpeakeasy [minor]
2 parents 2c1e1a0 + a0e713f commit 4dc5f71

6 files changed

Lines changed: 66 additions & 68 deletions

File tree

go/README.md

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,27 +36,27 @@ func main() {
3636

3737
## Rendering the callback URL
3838

39-
Guide content ships with the template key `{{ gram.oauth.callback_url }}`
40-
in place. Pass your own value on every lookup to substitute it:
39+
`Guide.External` and `Guide.Speakeasy` ship with the template key
40+
`{{ gram.oauth.callback_url }}` in place. **Serve the renderers, not the raw
41+
fields.** Pass your own value on every call:
4142

4243
```go
43-
g, _ := guides.Lookup("intercom")
44+
vars := guides.Vars{OAuthCallbackURL: "https://app.example.com/oauth/callback"}
4445

45-
g = g.Render(guides.Vars{
46-
OAuthCallbackURL: "https://app.example.com/oauth/callback",
47-
})
48-
os.Stdout.Write(g.External)
46+
g, _ := guides.Lookup("intercom")
47+
os.Stdout.Write(g.RenderExternal(vars))
4948
```
5049

51-
- `Render` returns a **copy**. It never changes the embedded content, so a
52-
server may render per request with a different value each time.
50+
- The renderers never change the embedded content, so a server may render per
51+
request with a different value each time. **Treat the result as read-only:**
52+
an empty `Vars` returns a slice aliasing the raw field.
5353
- The callback URL is a property of your deployment, not of the guide, so
54-
there is nothing to look up first. A guide that never references the key
54+
there is nothing to look up first. Content that never references the key
5555
comes back unchanged.
5656
- An empty `Vars` field leaves its key in place, so a missing value degrades
57-
to the unrendered guide instead of a blank.
58-
- `Render` substitutes `External` and `Speakeasy` only. No template key
59-
reaches `Meta` or an asset — the generator fails if one ever does.
57+
to the unrendered content instead of a blank.
58+
- Only `External` and `Speakeasy` have a renderer. No template key reaches
59+
`Meta` or an asset — the generator fails if one ever does.
6060
- `{{ gram.oauth.callback_url }}` is the only supported key. Add a field to
6161
`Vars` to support another; existing callers keep compiling.
6262

go/doc.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
// secondary indexes that may return zero, one, or many matches; they never
77
// invent a default remote.
88
//
9-
// Guide content ships with the template key {{ gram.oauth.callback_url }}
10-
// in place. Guide.Render(Vars) returns a copy with a caller-supplied value
11-
// substituted. Supply the value on every call: it is a property of the
12-
// deployment, not of the guide, and a guide that never references the key
13-
// comes back unchanged.
9+
// Guide.External and Guide.Speakeasy ship with the template key
10+
// {{ gram.oauth.callback_url }} in place. Serve Guide.RenderExternal(Vars)
11+
// and Guide.RenderSpeakeasy(Vars) instead: they return the same content
12+
// with a caller-supplied value substituted. Supply the value on every call.
13+
// It is a property of the deployment, not of the guide, and content that
14+
// never references the key comes back unchanged.
1415
//
1516
// Module path: github.com/speakeasy-api/mcp-setup-docs/go
1617
// Version tags: go/vX.Y.Z

go/guides_test.go

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -433,65 +433,61 @@ func TestQueryMechanisms(t *testing.T) {
433433
})
434434
}
435435

436-
// A caller supplies the callback URL on every Render, so run the whole
437-
// corpus through one call: no guide may keep the key, and a guide that
438-
// never carried it must come back byte-identical.
436+
// A caller supplies the callback URL on every render, so run both
437+
// renderers over the whole corpus: no output may keep the key, and content
438+
// that never carried it must come back byte-identical.
439439
func TestRenderSubstitutesEveryKey(t *testing.T) {
440440
const callback = "https://app.example.com/oauth/callback"
441441
key := []byte(canonicalCallbackKey)
442+
vars := guides.Vars{OAuthCallbackURL: callback}
442443
substituted := 0
443-
for _, g := range guides.Guides() {
444-
hadKey := bytes.Contains(g.External, key) || bytes.Contains(g.Speakeasy, key)
445-
out := g.Render(guides.Vars{OAuthCallbackURL: callback})
446-
447-
if bytes.Contains(out.External, key) || bytes.Contains(out.Speakeasy, key) {
448-
t.Errorf("%s: content still carries the key after Render", g.Slug)
444+
check := func(slug guides.GuideSlug, field string, raw, out []byte) {
445+
if bytes.Contains(out, key) {
446+
t.Errorf("%s %s: output still carries the key", slug, field)
449447
}
450-
if !bytes.Equal(out.Meta, g.Meta) {
451-
t.Errorf("%s: Render must not touch Meta", g.Slug)
452-
}
453-
454-
if !hadKey {
455-
if !bytes.Equal(out.External, g.External) || !bytes.Equal(out.Speakeasy, g.Speakeasy) {
456-
t.Errorf("%s: Render changed a guide that carries no key", g.Slug)
448+
if !bytes.Contains(raw, key) {
449+
if !bytes.Equal(out, raw) {
450+
t.Errorf("%s %s: content with no key changed", slug, field)
457451
}
458-
continue
452+
return
459453
}
460454
substituted++
461-
if !bytes.Contains(out.External, []byte(callback)) &&
462-
!bytes.Contains(out.Speakeasy, []byte(callback)) {
463-
t.Errorf("%s: Render substituted nothing", g.Slug)
455+
if !bytes.Contains(out, []byte(callback)) {
456+
t.Errorf("%s %s: substituted nothing", slug, field)
464457
}
465458
}
459+
for _, g := range guides.Guides() {
460+
check(g.Slug, "External", g.External, g.RenderExternal(vars))
461+
check(g.Slug, "Speakeasy", g.Speakeasy, g.RenderSpeakeasy(vars))
462+
}
466463
if substituted == 0 {
467464
t.Fatal("no guide carries the key; the matrix would be vacuous")
468465
}
469466
}
470467

471-
// A missing value must degrade to the unrendered guide, never to a blank
468+
// A missing value must degrade to the unrendered content, never to a blank
472469
// where the URL belongs.
473470
func TestRenderLeavesKeyWhenValueEmpty(t *testing.T) {
474471
g, ok := guides.Lookup("intercom")
475472
if !ok {
476473
t.Fatal("intercom missing")
477474
}
478-
out := g.Render(guides.Vars{})
479-
if !bytes.Contains(out.External, []byte(canonicalCallbackKey)) {
475+
if !bytes.Contains(g.RenderExternal(guides.Vars{}), []byte(canonicalCallbackKey)) {
480476
t.Error("empty Vars must leave the template key in place")
481477
}
482478
}
483479

484-
// The embedded bytes back every future Lookup, so rendering one copy must
485-
// not reach them. A caller that renders per request depends on this.
480+
// The embedded bytes back every future Lookup, so rendering must not reach
481+
// them. A caller that renders per request depends on this.
486482
func TestRenderDoesNotDisturbEmbeddedContent(t *testing.T) {
487483
g, ok := guides.Lookup("intercom")
488484
if !ok {
489485
t.Fatal("intercom missing")
490486
}
491487
before := string(g.External)
492-
_ = g.Render(guides.Vars{OAuthCallbackURL: "https://app.example.com/cb"})
488+
_ = g.RenderExternal(guides.Vars{OAuthCallbackURL: "https://app.example.com/cb"})
493489
if string(g.External) != before {
494-
t.Error("Render mutated the Guide it was called on")
490+
t.Error("a renderer changed the Guide it was called on")
495491
}
496492
again, ok := guides.Lookup("intercom")
497493
if !ok {

go/internal/gen/main.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ const canonicalCallbackKey = "{{ gram.oauth.callback_url }}"
2929

3030
// templateKeyPattern finds every "{{ … }}" span, including malformed
3131
// spacing, so a non-canonical key fails generation instead of shipping
32-
// unrendered. This check is what lets Render match one exact byte string.
32+
// unrendered. This check is what lets the renderers match one exact byte
33+
// string.
3334
var templateKeyPattern = regexp.MustCompile(`\{\{[^{}]*\}\}`)
3435

3536
type assetMeta struct {
@@ -417,9 +418,9 @@ func run() error {
417418
}
418419

419420
// scanTemplateKeys enforces the single-template-key rule. One canonical
420-
// spelling is what lets Guide.Render substitute with a plain byte replace
421-
// instead of a regexp. metaRaw must carry no key at all: Render substitutes
422-
// only External and Speakeasy, so a key in meta.yaml would ship to a reader
421+
// spelling is what lets package guides substitute with a plain byte replace
422+
// instead of a regexp. metaRaw must carry no key at all: only External and
423+
// Speakeasy have a renderer, so a key in meta.yaml would ship to a reader
423424
// unrendered.
424425
func scanTemplateKeys(slug, srcDir string, metaRaw []byte) error {
425426
if key := templateKeyPattern.Find(metaRaw); key != nil {

go/internal/gen/main_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,8 +251,8 @@ remotes:
251251
}
252252

253253
// The generator is the only place that enforces the one-key rule, so the
254-
// rejections matter as much as the happy path: Render substitutes with a
255-
// literal byte replacement on the strength of this check.
254+
// rejections matter as much as the happy path: the renderers substitute
255+
// with a literal byte replacement on the strength of this check.
256256
func TestScanTemplateKeys(t *testing.T) {
257257
for _, tc := range []struct {
258258
name string

go/render.go

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,27 @@ type Vars struct {
1313
// Speakeasy AI Control Plane callback URL that the reader registers
1414
// in the provider's redirect field.
1515
//
16-
// Supply it on every Render call. It is a property of the deployment,
17-
// not of the guide, so there is nothing to look up first: guides that
18-
// never reference it come back unchanged.
16+
// Supply it on every call. It is a property of the deployment, not of
17+
// the guide, so there is nothing to look up first: content that never
18+
// references it comes back unchanged.
1919
//
2020
// An empty value leaves the key in the content rather than blanking
21-
// it, so a missing value degrades to the unrendered guide.
21+
// it, so a missing value degrades to the unrendered content.
2222
OAuthCallbackURL string
2323
}
2424

25-
// Render returns a copy of g whose External and Speakeasy content has the
26-
// template keys replaced by the values in v. Meta, Assets, and every
27-
// identity field are unchanged — no template key appears in meta.yaml or
28-
// in an asset, and the generator fails if one ever does.
29-
//
30-
// Render never changes the embedded content. Each Lookup starts from the
31-
// unrendered bytes, and rendering one copy does not affect another.
32-
func (g Guide) Render(v Vars) Guide {
33-
if v.OAuthCallbackURL != "" {
34-
value := []byte(v.OAuthCallbackURL)
35-
g.External = bytes.ReplaceAll(g.External, callbackURLKey, value)
36-
g.Speakeasy = bytes.ReplaceAll(g.Speakeasy, callbackURLKey, value)
25+
// RenderExternal returns External with the template keys replaced by the
26+
// values in v. Serve this rather than the raw field: the raw field still
27+
// carries the keys. Treat the result as read-only; it may alias External.
28+
func (g Guide) RenderExternal(v Vars) []byte { return render(g.External, v) }
29+
30+
// RenderSpeakeasy returns Speakeasy with the template keys replaced. See
31+
// RenderExternal.
32+
func (g Guide) RenderSpeakeasy(v Vars) []byte { return render(g.Speakeasy, v) }
33+
34+
func render(content []byte, v Vars) []byte {
35+
if v.OAuthCallbackURL == "" {
36+
return content
3737
}
38-
return g
38+
return bytes.ReplaceAll(content, callbackURLKey, []byte(v.OAuthCallbackURL))
3939
}

0 commit comments

Comments
 (0)