@pi-in-go/pigpen-pi-typesafe 0.1.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.
- package/CREDITS.md +14 -0
- package/LICENSE +22 -0
- package/README.md +45 -0
- package/extensions/pi-typesafe/branches_test.go +185 -0
- package/extensions/pi-typesafe/command.go +319 -0
- package/extensions/pi-typesafe/export_test.go +9 -0
- package/extensions/pi-typesafe/extension.go +188 -0
- package/extensions/pi-typesafe/extension_test.go +321 -0
- package/extensions/pi-typesafe/fakehost_test.go +548 -0
- package/extensions/pi-typesafe/format.go +191 -0
- package/extensions/pi-typesafe/format_test.go +75 -0
- package/extensions/pi-typesafe/go.mod +10 -0
- package/extensions/pi-typesafe/go.sum +2 -0
- package/extensions/pi-typesafe/go.work +11 -0
- package/extensions/pi-typesafe/harness_test.go +200 -0
- package/extensions/pi-typesafe/ownmodel_test.go +100 -0
- package/extensions/pi-typesafe/review_test.go +134 -0
- package/extensions/pi-typesafe/tool.go +193 -0
- package/extensions/pi-typesafe/twin_test.go +28 -0
- package/libs/pi-typesafe-api/CREDITS.md +14 -0
- package/libs/pi-typesafe-api/LICENSE +22 -0
- package/libs/pi-typesafe-api/README.md +30 -0
- package/libs/pi-typesafe-api/ask.go +62 -0
- package/libs/pi-typesafe-api/ask_test.go +76 -0
- package/libs/pi-typesafe-api/auth.go +249 -0
- package/libs/pi-typesafe-api/auth_test.go +131 -0
- package/libs/pi-typesafe-api/backends.go +336 -0
- package/libs/pi-typesafe-api/backends_test.go +404 -0
- package/libs/pi-typesafe-api/batch.go +202 -0
- package/libs/pi-typesafe-api/batch_test.go +202 -0
- package/libs/pi-typesafe-api/battery_test.go +41 -0
- package/libs/pi-typesafe-api/calibrate.go +354 -0
- package/libs/pi-typesafe-api/calibrate_test.go +186 -0
- package/libs/pi-typesafe-api/client.go +615 -0
- package/libs/pi-typesafe-api/client_test.go +490 -0
- package/libs/pi-typesafe-api/credentials.go +252 -0
- package/libs/pi-typesafe-api/credentials_test.go +216 -0
- package/libs/pi-typesafe-api/doc.go +14 -0
- package/libs/pi-typesafe-api/errors.go +143 -0
- package/libs/pi-typesafe-api/evaluation.go +86 -0
- package/libs/pi-typesafe-api/evaluation_schema.json +264 -0
- package/libs/pi-typesafe-api/gaps_test.go +77 -0
- package/libs/pi-typesafe-api/go.mod +9 -0
- package/libs/pi-typesafe-api/go.sum +2 -0
- package/libs/pi-typesafe-api/helpers_test.go +169 -0
- package/libs/pi-typesafe-api/hostmodel/hostmodel.go +87 -0
- package/libs/pi-typesafe-api/json.go +299 -0
- package/libs/pi-typesafe-api/json_test.go +92 -0
- package/libs/pi-typesafe-api/ownmodel_test.go +79 -0
- package/libs/pi-typesafe-api/package.json +40 -0
- package/libs/pi-typesafe-api/provenance.json +18 -0
- package/libs/pi-typesafe-api/review_test.go +23 -0
- package/libs/pi-typesafe-api/schema.go +473 -0
- package/libs/pi-typesafe-api/schema_test.go +262 -0
- package/libs/pi-typesafe-api/testdata/tools/typebox-messages.mts +5 -0
- package/libs/pi-typesafe-api/testdata/typebox-messages.json +285 -0
- package/libs/pi-typesafe-api/twin_test.go +28 -0
- package/libs/pi-typesafe-api/ui/fakehost_test.go +548 -0
- package/libs/pi-typesafe-api/ui/keyprompt.go +115 -0
- package/libs/pi-typesafe-api/ui/login.go +106 -0
- package/libs/pi-typesafe-api/ui/twin_test.go +28 -0
- package/libs/pi-typesafe-api/ui/ui_test.go +285 -0
- package/libs/pi-typesafe-api/usage.go +366 -0
- package/libs/pi-typesafe-api/usage_test.go +139 -0
- package/libs/typesafe/CONTRACT.md +125 -0
- package/libs/typesafe/CREDITS.md +37 -0
- package/libs/typesafe/LICENSE +23 -0
- package/libs/typesafe/README.md +19 -0
- package/libs/typesafe/go.mod +3 -0
- package/libs/typesafe/libraries/ownmodel/backend_test.go +496 -0
- package/libs/typesafe/libraries/ownmodel/canon.go +190 -0
- package/libs/typesafe/libraries/ownmodel/convert.go +199 -0
- package/libs/typesafe/libraries/ownmodel/doc.go +15 -0
- package/libs/typesafe/libraries/ownmodel/equivalence_test.go +199 -0
- package/libs/typesafe/libraries/ownmodel/helpers_test.go +155 -0
- package/libs/typesafe/libraries/ownmodel/mutation_test.go +31 -0
- package/libs/typesafe/libraries/ownmodel/ownmodel.go +225 -0
- package/libs/typesafe/libraries/ownmodel/plan.go +442 -0
- package/libs/typesafe/libraries/ownmodel/run.go +288 -0
- package/libs/typesafe/libraries/ownmodel/schema_test.go +254 -0
- package/libs/typesafe/libraries/ownmodel/twins_test.go +169 -0
- package/libs/typesafe/libraries/ownmodel/utils_test.go +125 -0
- package/libs/typesafe/libraries/pigmodel/pigmodel.go +264 -0
- package/libs/typesafe/libraries/pigmodel/pigmodel_test.go +410 -0
- package/libs/typesafe/libraries/typesafe/answers.go +268 -0
- package/libs/typesafe/libraries/typesafe/api_response_test.go +113 -0
- package/libs/typesafe/libraries/typesafe/batch.go +80 -0
- package/libs/typesafe/libraries/typesafe/batch_test.go +133 -0
- package/libs/typesafe/libraries/typesafe/bench_test.go +71 -0
- package/libs/typesafe/libraries/typesafe/client.go +561 -0
- package/libs/typesafe/libraries/typesafe/client_test.go +495 -0
- package/libs/typesafe/libraries/typesafe/crosscheck_test.go +464 -0
- package/libs/typesafe/libraries/typesafe/crosscheck_workflowevals_test.go +219 -0
- package/libs/typesafe/libraries/typesafe/doc.go +27 -0
- package/libs/typesafe/libraries/typesafe/entry.go +142 -0
- package/libs/typesafe/libraries/typesafe/env.go +11 -0
- package/libs/typesafe/libraries/typesafe/errors.go +310 -0
- package/libs/typesafe/libraries/typesafe/errors_test.go +175 -0
- package/libs/typesafe/libraries/typesafe/helpers_test.go +294 -0
- package/libs/typesafe/libraries/typesafe/live_test.go +96 -0
- package/libs/typesafe/libraries/typesafe/logging.go +160 -0
- package/libs/typesafe/libraries/typesafe/logging_test.go +259 -0
- package/libs/typesafe/libraries/typesafe/marshal_test.go +112 -0
- package/libs/typesafe/libraries/typesafe/mutation_test.go +39 -0
- package/libs/typesafe/libraries/typesafe/questions.go +490 -0
- package/libs/typesafe/libraries/typesafe/questions_test.go +166 -0
- package/libs/typesafe/libraries/typesafe/regressions_test.go +159 -0
- package/libs/typesafe/libraries/typesafe/reliability_test.go +649 -0
- package/libs/typesafe/libraries/typesafe/retry.go +350 -0
- package/libs/typesafe/libraries/typesafe/retry_test.go +297 -0
- package/libs/typesafe/libraries/typesafe/runtime_test.go +26 -0
- package/libs/typesafe/libraries/typesafe/transport_test.go +163 -0
- package/libs/typesafe/libraries/typesafe/twins_test.go +127 -0
- package/libs/typesafe/libraries/typesafe/types_test.go +165 -0
- package/libs/typesafe/libraries/typesafe/version.go +10 -0
- package/libs/typesafe/package.json +37 -0
- package/libs/typesafe/provenance.json +49 -0
- package/package.json +42 -0
- package/port/PORT.md +98 -0
- package/port/accepted-gaps.json +3 -0
- package/port/golden/enable-confirm.jsonl +11 -0
- package/port/golden/enable-decline.jsonl +20 -0
- package/port/golden/enable-missing-key.jsonl +4 -0
- package/port/golden/login-shadow.jsonl +4 -0
- package/port/golden/logout-env-key.jsonl +6 -0
- package/port/golden/playground-cancel.jsonl +4 -0
- package/port/golden/playground-invalid-json.jsonl +5 -0
- package/port/golden/playground-invalid-questions.jsonl +5 -0
- package/port/golden/status-env-key.jsonl +6 -0
- package/port/golden/status-no-key.jsonl +6 -0
- package/port/golden/tool-disabled.jsonl +18 -0
- package/port/golden/trailing-words.jsonl +10 -0
- package/port/library-mutations.py +44 -0
- package/port/mutations.json +302 -0
- package/port/oracle/.env.example +4 -0
- package/port/oracle/CHANGELOG.md +91 -0
- package/port/oracle/CONTRIBUTING.md +35 -0
- package/port/oracle/LICENSE +21 -0
- package/port/oracle/README.md +159 -0
- package/port/oracle/docs/api.md +143 -0
- package/port/oracle/docs/ci-cd.md +97 -0
- package/port/oracle/examples/decision-extension.ts +41 -0
- package/port/oracle/extensions/index.js +2 -0
- package/port/oracle/package.json +89 -0
- package/port/oracle/scripts/dev-pi.mjs +23 -0
- package/port/oracle/scripts/live-smoke.mjs +35 -0
- package/port/oracle/src/ask.ts +42 -0
- package/port/oracle/src/auth.ts +171 -0
- package/port/oracle/src/backends.ts +196 -0
- package/port/oracle/src/batch.ts +170 -0
- package/port/oracle/src/calibrate.ts +237 -0
- package/port/oracle/src/client.ts +310 -0
- package/port/oracle/src/credentials.ts +136 -0
- package/port/oracle/src/errors.ts +53 -0
- package/port/oracle/src/extension.ts +204 -0
- package/port/oracle/src/index.ts +31 -0
- package/port/oracle/src/key-prompt.ts +51 -0
- package/port/oracle/src/login.ts +60 -0
- package/port/oracle/src/schema.ts +158 -0
- package/port/oracle/src/ui.ts +4 -0
- package/port/oracle/src/usage.ts +258 -0
- package/port/oracle/tests/ask.test.ts +63 -0
- package/port/oracle/tests/auth.test.ts +141 -0
- package/port/oracle/tests/backends.test.ts +380 -0
- package/port/oracle/tests/batch.test.ts +156 -0
- package/port/oracle/tests/calibrate.test.ts +144 -0
- package/port/oracle/tests/client.test.ts +499 -0
- package/port/oracle/tests/credentials.test.ts +144 -0
- package/port/oracle/tests/extension.test.ts +276 -0
- package/port/oracle/tests/key-prompt.test.ts +47 -0
- package/port/oracle/tests/login.test.ts +101 -0
- package/port/oracle/tests/schema.test.ts +85 -0
- package/port/oracle/tests/usage.test.ts +106 -0
- package/port/oracle/tsconfig.build.json +10 -0
- package/port/oracle/tsconfig.json +14 -0
- package/port/scenarios/enable-confirm.json +5 -0
- package/port/scenarios/enable-decline.json +3 -0
- package/port/scenarios/enable-missing-key.json +2 -0
- package/port/scenarios/login-shadow.json +2 -0
- package/port/scenarios/logout-env-key.json +3 -0
- package/port/scenarios/playground-cancel.json +2 -0
- package/port/scenarios/playground-invalid-json.json +2 -0
- package/port/scenarios/playground-invalid-questions.json +2 -0
- package/port/scenarios/status-env-key.json +3 -0
- package/port/scenarios/status-no-key.json +3 -0
- package/port/scenarios/tool-disabled.json +2 -0
- package/port/scenarios/trailing-words.json +5 -0
- package/port/upstream-tests.json +160 -0
- package/provenance.json +18 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
package typesafe
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"errors"
|
|
7
|
+
"fmt"
|
|
8
|
+
"io"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// QuestionType names a kind of question on the wire (the "type" field).
|
|
12
|
+
type QuestionType string
|
|
13
|
+
|
|
14
|
+
// The three question kinds.
|
|
15
|
+
const (
|
|
16
|
+
TypeNoul QuestionType = "noul"
|
|
17
|
+
TypeChoice QuestionType = "choice"
|
|
18
|
+
TypeScore QuestionType = "score"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
// Question is a yes/no ([NoulQuestion]), named-alternatives ([ChoiceQuestion]) or
|
|
22
|
+
// ordered-rubric ([ScoreQuestion]) question. The interface is sealed to this package.
|
|
23
|
+
type Question interface {
|
|
24
|
+
// QuestionType returns the wire type of the question.
|
|
25
|
+
QuestionType() QuestionType
|
|
26
|
+
json.Marshaler
|
|
27
|
+
isQuestion()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// NoulCriteria describes the yes ("true") and no ("false") outcomes. Each side is
|
|
31
|
+
// omitted, null or a description independently. Null makes the whole criteria value
|
|
32
|
+
// null on the wire (the TypeScript `noul(x, null)`).
|
|
33
|
+
type NoulCriteria struct {
|
|
34
|
+
True Entry
|
|
35
|
+
False Entry
|
|
36
|
+
Null bool
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// MarshalJSON writes null when Null is set, else an object with the sides that are not omitted.
|
|
40
|
+
func (c NoulCriteria) MarshalJSON() ([]byte, error) {
|
|
41
|
+
if c.Null {
|
|
42
|
+
return []byte("null"), nil
|
|
43
|
+
}
|
|
44
|
+
o := newObject()
|
|
45
|
+
o.entry("true", c.True)
|
|
46
|
+
o.entry("false", c.False)
|
|
47
|
+
return o.done()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// NoulQuestion is a yes/no question. Instructions nil-valued (zero) is omitted from
|
|
51
|
+
// the wire; the [Noul] builder sets null, like the TypeScript builder.
|
|
52
|
+
type NoulQuestion struct {
|
|
53
|
+
Instructions Entry
|
|
54
|
+
// Criteria is nil when omitted from the request.
|
|
55
|
+
Criteria *NoulCriteria
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Noul builds a yes/no question; instructions is text, JSON or nil (null).
|
|
59
|
+
func Noul(instructions any) NoulQuestion { return NoulQuestion{Instructions: EntryOf(instructions)} }
|
|
60
|
+
|
|
61
|
+
func (q NoulQuestion) withCriteria(mutate func(*NoulCriteria)) NoulQuestion {
|
|
62
|
+
c := NoulCriteria{}
|
|
63
|
+
if q.Criteria != nil {
|
|
64
|
+
c = *q.Criteria
|
|
65
|
+
}
|
|
66
|
+
c.Null = false
|
|
67
|
+
mutate(&c)
|
|
68
|
+
q.Criteria = &c
|
|
69
|
+
return q
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Yes returns a copy that describes the yes outcome; nil describes it as null.
|
|
73
|
+
func (q NoulQuestion) Yes(description any) NoulQuestion {
|
|
74
|
+
return q.withCriteria(func(c *NoulCriteria) { c.True = EntryOf(description) })
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// No returns a copy that describes the no outcome; nil describes it as null.
|
|
78
|
+
func (q NoulQuestion) No(description any) NoulQuestion {
|
|
79
|
+
return q.withCriteria(func(c *NoulCriteria) { c.False = EntryOf(description) })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// NullCriteria returns a copy whose criteria are sent as null.
|
|
83
|
+
func (q NoulQuestion) NullCriteria() NoulQuestion {
|
|
84
|
+
q.Criteria = &NoulCriteria{Null: true}
|
|
85
|
+
return q
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// QuestionType implements [Question].
|
|
89
|
+
func (NoulQuestion) QuestionType() QuestionType { return TypeNoul }
|
|
90
|
+
func (NoulQuestion) isQuestion() {}
|
|
91
|
+
|
|
92
|
+
// jsonObject writes a JSON object with its keys in insertion order.
|
|
93
|
+
type jsonObject struct {
|
|
94
|
+
buf bytes.Buffer
|
|
95
|
+
n int
|
|
96
|
+
err error
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
func newObject() *jsonObject {
|
|
100
|
+
o := &jsonObject{}
|
|
101
|
+
o.buf.WriteByte('{')
|
|
102
|
+
return o
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
func (o *jsonObject) key(name string) {
|
|
106
|
+
if o.n > 0 {
|
|
107
|
+
o.buf.WriteByte(',')
|
|
108
|
+
}
|
|
109
|
+
o.n++
|
|
110
|
+
if plainKey(name) {
|
|
111
|
+
o.buf.WriteByte('"')
|
|
112
|
+
o.buf.WriteString(name)
|
|
113
|
+
o.buf.WriteByte('"')
|
|
114
|
+
} else {
|
|
115
|
+
k, _ := marshalPlain(name)
|
|
116
|
+
o.buf.Write(k)
|
|
117
|
+
}
|
|
118
|
+
o.buf.WriteByte(':')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// plainKey reports whether name is printable ASCII that JSON writes as itself between quotes: no quote,
|
|
122
|
+
// backslash, control character, DEL, or HTML-sensitive character (marshalPlain would leave those alone, but the
|
|
123
|
+
// slow path is kept for anything not obviously safe).
|
|
124
|
+
func plainKey(name string) bool {
|
|
125
|
+
for i := 0; i < len(name); i++ {
|
|
126
|
+
if c := name[i]; c < 0x20 || c >= 0x7f || c == '"' || c == '\\' || c == '<' || c == '>' || c == '&' {
|
|
127
|
+
return false
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return true
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
func (o *jsonObject) raw(name string, raw []byte) {
|
|
134
|
+
o.key(name)
|
|
135
|
+
o.buf.Write(raw)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
func (o *jsonObject) value(name string, v any) {
|
|
139
|
+
raw, err := marshalPlain(v)
|
|
140
|
+
if err != nil {
|
|
141
|
+
if o.err == nil {
|
|
142
|
+
o.err = unwrapMarshalError(err)
|
|
143
|
+
}
|
|
144
|
+
raw = []byte("null")
|
|
145
|
+
}
|
|
146
|
+
o.raw(name, raw)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
func (o *jsonObject) entry(name string, e Entry) {
|
|
150
|
+
if e.IsOmitted() {
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
o.value(name, e)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
func (o *jsonObject) done() ([]byte, error) {
|
|
157
|
+
if o.err != nil {
|
|
158
|
+
return nil, o.err
|
|
159
|
+
}
|
|
160
|
+
o.buf.WriteByte('}')
|
|
161
|
+
return o.buf.Bytes(), nil
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// MarshalJSON implements [Question].
|
|
165
|
+
func (q NoulQuestion) MarshalJSON() ([]byte, error) {
|
|
166
|
+
o := newObject()
|
|
167
|
+
o.value("type", TypeNoul)
|
|
168
|
+
o.entry("instructions", q.Instructions)
|
|
169
|
+
if q.Criteria != nil {
|
|
170
|
+
raw, err := q.Criteria.MarshalJSON()
|
|
171
|
+
if err != nil {
|
|
172
|
+
return nil, err
|
|
173
|
+
}
|
|
174
|
+
o.raw("criteria", raw)
|
|
175
|
+
}
|
|
176
|
+
return o.done()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Option is one choice label with its description (omitted or nil is sent as null).
|
|
180
|
+
type Option struct {
|
|
181
|
+
Label string
|
|
182
|
+
Description Entry
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Opt builds an Option; description is text, JSON or nil (null).
|
|
186
|
+
func Opt(label string, description any) Option {
|
|
187
|
+
return Option{Label: label, Description: EntryOf(description)}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ChoiceQuestion selects between named alternatives. Options keep their order on the wire.
|
|
191
|
+
type ChoiceQuestion struct {
|
|
192
|
+
Instructions Entry
|
|
193
|
+
Criteria []Option
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Choice builds a question that selects between named alternatives.
|
|
197
|
+
func Choice(instructions any, options ...Option) ChoiceQuestion {
|
|
198
|
+
return ChoiceQuestion{Instructions: EntryOf(instructions), Criteria: append([]Option(nil), options...)}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// QuestionType implements [Question].
|
|
202
|
+
func (ChoiceQuestion) QuestionType() QuestionType { return TypeChoice }
|
|
203
|
+
func (ChoiceQuestion) isQuestion() {}
|
|
204
|
+
|
|
205
|
+
// MarshalJSON implements [Question]; Criteria is written as a JSON object in order.
|
|
206
|
+
func (q ChoiceQuestion) MarshalJSON() ([]byte, error) {
|
|
207
|
+
o := newObject()
|
|
208
|
+
o.value("type", TypeChoice)
|
|
209
|
+
o.entry("instructions", q.Instructions)
|
|
210
|
+
sub := newObject()
|
|
211
|
+
for _, opt := range q.Criteria {
|
|
212
|
+
sub.value(opt.Label, opt.Description)
|
|
213
|
+
}
|
|
214
|
+
raw, err := sub.done()
|
|
215
|
+
if err != nil {
|
|
216
|
+
return nil, err
|
|
217
|
+
}
|
|
218
|
+
o.raw("criteria", raw)
|
|
219
|
+
return o.done()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ScoreQuestion assigns a score from an ordered rubric: Criteria[i] describes score i.
|
|
223
|
+
// At least two entries are required ([Questions.Validate]).
|
|
224
|
+
type ScoreQuestion struct {
|
|
225
|
+
Instructions Entry
|
|
226
|
+
Criteria []Entry
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Score builds a score question; each criterion is text, JSON or nil (null).
|
|
230
|
+
func Score(instructions any, criteria ...any) ScoreQuestion {
|
|
231
|
+
q := ScoreQuestion{Instructions: EntryOf(instructions), Criteria: make([]Entry, len(criteria))}
|
|
232
|
+
for i, c := range criteria {
|
|
233
|
+
q.Criteria[i] = EntryOf(c)
|
|
234
|
+
}
|
|
235
|
+
return q
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// QuestionType implements [Question].
|
|
239
|
+
func (ScoreQuestion) QuestionType() QuestionType { return TypeScore }
|
|
240
|
+
func (ScoreQuestion) isQuestion() {}
|
|
241
|
+
|
|
242
|
+
// MarshalJSON implements [Question].
|
|
243
|
+
func (q ScoreQuestion) MarshalJSON() ([]byte, error) {
|
|
244
|
+
o := newObject()
|
|
245
|
+
o.value("type", TypeScore)
|
|
246
|
+
o.entry("instructions", q.Instructions)
|
|
247
|
+
criteria := q.Criteria
|
|
248
|
+
if criteria == nil {
|
|
249
|
+
criteria = []Entry{}
|
|
250
|
+
}
|
|
251
|
+
o.value("criteria", criteria)
|
|
252
|
+
return o.done()
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// NamedQuestion is a question with the name that identifies its answer.
|
|
256
|
+
type NamedQuestion struct {
|
|
257
|
+
Name string
|
|
258
|
+
Question Question
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Ask names a question.
|
|
262
|
+
func Ask(name string, q Question) NamedQuestion { return NamedQuestion{Name: name, Question: q} }
|
|
263
|
+
|
|
264
|
+
// Questions is an ordered set of named questions; the order is kept on the wire and in
|
|
265
|
+
// prompts. Names must be unique.
|
|
266
|
+
type Questions []NamedQuestion
|
|
267
|
+
|
|
268
|
+
// Validate rejects an empty set, duplicate names, and score questions with fewer than
|
|
269
|
+
// two criteria, before anything is sent (the TypeScript validateQuestions).
|
|
270
|
+
func (qs Questions) Validate() error {
|
|
271
|
+
if len(qs) == 0 {
|
|
272
|
+
return errorf("At least one question is required.")
|
|
273
|
+
}
|
|
274
|
+
seen := make(map[string]bool, len(qs))
|
|
275
|
+
for _, nq := range qs {
|
|
276
|
+
if seen[nq.Name] {
|
|
277
|
+
return errorf("Duplicate question name %q.", nq.Name)
|
|
278
|
+
}
|
|
279
|
+
seen[nq.Name] = true
|
|
280
|
+
if nq.Question == nil {
|
|
281
|
+
return errorf("Question %q is nil.", nq.Name)
|
|
282
|
+
}
|
|
283
|
+
if s, ok := nq.Question.(ScoreQuestion); ok && len(s.Criteria) < 2 {
|
|
284
|
+
return errorf("Score question %q has %d criteria; at least two scores are required.", nq.Name, len(s.Criteria))
|
|
285
|
+
}
|
|
286
|
+
if c, ok := nq.Question.(ChoiceQuestion); ok {
|
|
287
|
+
labels := make(map[string]bool, len(c.Criteria))
|
|
288
|
+
for _, opt := range c.Criteria {
|
|
289
|
+
if labels[opt.Label] {
|
|
290
|
+
return errorf("Choice question %q has the label %q twice.", nq.Name, opt.Label)
|
|
291
|
+
}
|
|
292
|
+
labels[opt.Label] = true
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return nil
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// MarshalJSON writes the set as a JSON object keyed by name, in order.
|
|
300
|
+
func (qs Questions) MarshalJSON() ([]byte, error) {
|
|
301
|
+
o := newObject()
|
|
302
|
+
for _, nq := range qs {
|
|
303
|
+
if nq.Question == nil {
|
|
304
|
+
return nil, errorf("Question %q is nil.", nq.Name)
|
|
305
|
+
}
|
|
306
|
+
o.value(nq.Name, nq.Question)
|
|
307
|
+
}
|
|
308
|
+
return o.done()
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ParseQuestions decodes the JSON object form (as written by MarshalJSON, or by hand
|
|
312
|
+
// or by a tool caller) into an ordered set, keeping key order and the omitted/null
|
|
313
|
+
// distinctions. Score criteria must be a list and choice criteria a map, as in the
|
|
314
|
+
// TypeScript builders.
|
|
315
|
+
func ParseQuestions(data []byte) (Questions, error) {
|
|
316
|
+
dec := json.NewDecoder(bytes.NewReader(data))
|
|
317
|
+
tok, err := dec.Token()
|
|
318
|
+
if err != nil {
|
|
319
|
+
return nil, &TypeSafeError{Message: "Questions must be a JSON object: " + err.Error(), Cause: err}
|
|
320
|
+
}
|
|
321
|
+
if d, ok := tok.(json.Delim); !ok || d != '{' {
|
|
322
|
+
return nil, errorf("Questions must be a JSON object keyed by question name.")
|
|
323
|
+
}
|
|
324
|
+
var out Questions
|
|
325
|
+
seen := map[string]bool{}
|
|
326
|
+
for dec.More() {
|
|
327
|
+
keyTok, err := dec.Token()
|
|
328
|
+
if err != nil {
|
|
329
|
+
return nil, &TypeSafeError{Message: "Questions must be a JSON object: " + err.Error(), Cause: err}
|
|
330
|
+
}
|
|
331
|
+
name := keyTok.(string)
|
|
332
|
+
var raw json.RawMessage
|
|
333
|
+
if err := dec.Decode(&raw); err != nil {
|
|
334
|
+
return nil, &TypeSafeError{Message: fmt.Sprintf("Question %q: %v", name, err), Cause: err}
|
|
335
|
+
}
|
|
336
|
+
if seen[name] {
|
|
337
|
+
return nil, errorf("Duplicate question name %q.", name)
|
|
338
|
+
}
|
|
339
|
+
seen[name] = true
|
|
340
|
+
q, err := parseQuestion(name, raw)
|
|
341
|
+
if err != nil {
|
|
342
|
+
return nil, err
|
|
343
|
+
}
|
|
344
|
+
out = append(out, NamedQuestion{Name: name, Question: q})
|
|
345
|
+
}
|
|
346
|
+
if _, err := dec.Token(); err != nil {
|
|
347
|
+
return nil, &TypeSafeError{Message: "Questions must be a JSON object: " + err.Error(), Cause: err}
|
|
348
|
+
}
|
|
349
|
+
if rest, err := dec.Token(); err != io.EOF {
|
|
350
|
+
return nil, errorf("Unexpected data after the questions object: %v", rest)
|
|
351
|
+
}
|
|
352
|
+
return out, nil
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
func parseQuestion(name string, raw json.RawMessage) (Question, error) {
|
|
356
|
+
var fields map[string]json.RawMessage
|
|
357
|
+
if err := json.Unmarshal(raw, &fields); err != nil || fields == nil {
|
|
358
|
+
return nil, errorf("Question %q must be a JSON object with a \"type\".", name)
|
|
359
|
+
}
|
|
360
|
+
var qtype string
|
|
361
|
+
if t, ok := fields["type"]; ok {
|
|
362
|
+
if err := json.Unmarshal(t, &qtype); err != nil {
|
|
363
|
+
return nil, errorf("Question %q: \"type\" must be text.", name)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
entry := func(field string) (Entry, error) {
|
|
367
|
+
v, ok := fields[field]
|
|
368
|
+
if !ok {
|
|
369
|
+
return Entry{}, nil
|
|
370
|
+
}
|
|
371
|
+
var e Entry
|
|
372
|
+
if err := json.Unmarshal(v, &e); err != nil {
|
|
373
|
+
return Entry{}, errorf("Question %q: %s must be text, a JSON object, a JSON array or null.", name, field)
|
|
374
|
+
}
|
|
375
|
+
return e, nil
|
|
376
|
+
}
|
|
377
|
+
instructions, err := entry("instructions")
|
|
378
|
+
if err != nil {
|
|
379
|
+
return nil, err
|
|
380
|
+
}
|
|
381
|
+
switch QuestionType(qtype) {
|
|
382
|
+
case TypeNoul:
|
|
383
|
+
q := NoulQuestion{Instructions: instructions}
|
|
384
|
+
if c, ok := fields["criteria"]; ok {
|
|
385
|
+
crit, err := parseNoulCriteria(name, c)
|
|
386
|
+
if err != nil {
|
|
387
|
+
return nil, err
|
|
388
|
+
}
|
|
389
|
+
q.Criteria = crit
|
|
390
|
+
}
|
|
391
|
+
return q, nil
|
|
392
|
+
case TypeChoice:
|
|
393
|
+
opts, err := parseChoiceCriteria(name, fields["criteria"])
|
|
394
|
+
if err != nil {
|
|
395
|
+
return nil, err
|
|
396
|
+
}
|
|
397
|
+
return ChoiceQuestion{Instructions: instructions, Criteria: opts}, nil
|
|
398
|
+
case TypeScore:
|
|
399
|
+
var list []json.RawMessage
|
|
400
|
+
c := bytes.TrimSpace(fields["criteria"])
|
|
401
|
+
if len(c) == 0 || c[0] != '[' {
|
|
402
|
+
if len(c) > 0 && c[0] == '{' {
|
|
403
|
+
return nil, errorf("Question %q: Score criteria must be a list of descriptions indexed by score from zero, not a map.", name)
|
|
404
|
+
}
|
|
405
|
+
return nil, errorf("Question %q: score criteria must be a list of descriptions indexed by score from zero.", name)
|
|
406
|
+
}
|
|
407
|
+
if err := json.Unmarshal(c, &list); err != nil {
|
|
408
|
+
return nil, errorf("Question %q: %v", name, err)
|
|
409
|
+
}
|
|
410
|
+
q := ScoreQuestion{Instructions: instructions, Criteria: make([]Entry, len(list))}
|
|
411
|
+
for i, item := range list {
|
|
412
|
+
if err := json.Unmarshal(item, &q.Criteria[i]); err != nil {
|
|
413
|
+
return nil, errorf("Question %q: criteria[%d] must be text, a JSON object, a JSON array or null.", name, i)
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return q, nil
|
|
417
|
+
case "":
|
|
418
|
+
return nil, errorf("Question %q needs a \"type\": noul, choice or score.", name)
|
|
419
|
+
}
|
|
420
|
+
return nil, errorf("Question %q has unknown type %q.", name, qtype)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
func parseNoulCriteria(name string, raw json.RawMessage) (*NoulCriteria, error) {
|
|
424
|
+
raw = bytes.TrimSpace(raw)
|
|
425
|
+
if string(raw) == "null" {
|
|
426
|
+
return &NoulCriteria{Null: true}, nil
|
|
427
|
+
}
|
|
428
|
+
var sides map[string]json.RawMessage
|
|
429
|
+
if len(raw) == 0 || raw[0] != '{' || json.Unmarshal(raw, &sides) != nil {
|
|
430
|
+
return nil, errorf("Question %q: noul criteria must be an object with \"true\" and \"false\", or null.", name)
|
|
431
|
+
}
|
|
432
|
+
c := &NoulCriteria{}
|
|
433
|
+
for side, dst := range map[string]*Entry{"true": &c.True, "false": &c.False} {
|
|
434
|
+
if v, ok := sides[side]; ok {
|
|
435
|
+
if err := json.Unmarshal(v, dst); err != nil {
|
|
436
|
+
return nil, errorf("Question %q: criteria.%s must be text, a JSON object, a JSON array or null.", name, side)
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return c, nil
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
func parseChoiceCriteria(name string, raw json.RawMessage) ([]Option, error) {
|
|
444
|
+
raw = bytes.TrimSpace(raw)
|
|
445
|
+
if len(raw) > 0 && raw[0] == '[' {
|
|
446
|
+
return nil, errorf("Question %q: Choice criteria must be a map of labels to descriptions, not a list.", name)
|
|
447
|
+
}
|
|
448
|
+
if len(raw) == 0 || raw[0] != '{' {
|
|
449
|
+
return nil, errorf("Question %q: choice criteria must be a map of labels to descriptions.", name)
|
|
450
|
+
}
|
|
451
|
+
dec := json.NewDecoder(bytes.NewReader(raw))
|
|
452
|
+
if _, err := dec.Token(); err != nil {
|
|
453
|
+
return nil, errorf("Question %q: %v", name, err)
|
|
454
|
+
}
|
|
455
|
+
var opts []Option
|
|
456
|
+
seen := map[string]bool{}
|
|
457
|
+
for dec.More() {
|
|
458
|
+
keyTok, err := dec.Token()
|
|
459
|
+
if err != nil {
|
|
460
|
+
return nil, errorf("Question %q: %v", name, err)
|
|
461
|
+
}
|
|
462
|
+
label := keyTok.(string)
|
|
463
|
+
var item json.RawMessage
|
|
464
|
+
if err := dec.Decode(&item); err != nil {
|
|
465
|
+
return nil, errorf("Question %q: %v", name, err)
|
|
466
|
+
}
|
|
467
|
+
if seen[label] {
|
|
468
|
+
return nil, errorf("Question %q has the label %q twice.", name, label)
|
|
469
|
+
}
|
|
470
|
+
seen[label] = true
|
|
471
|
+
var e Entry
|
|
472
|
+
if err := json.Unmarshal(item, &e); err != nil {
|
|
473
|
+
return nil, errorf("Question %q: the description of %q must be text, a JSON object, a JSON array or null.", name, label)
|
|
474
|
+
}
|
|
475
|
+
opts = append(opts, Option{Label: label, Description: e})
|
|
476
|
+
}
|
|
477
|
+
return opts, nil
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// unwrapMarshalError returns the error a MarshalJSON method returned, without the
|
|
481
|
+
// json.MarshalerError layers that encoding/json adds around it.
|
|
482
|
+
func unwrapMarshalError(err error) error {
|
|
483
|
+
for {
|
|
484
|
+
var me *json.MarshalerError
|
|
485
|
+
if !errors.As(err, &me) {
|
|
486
|
+
return err
|
|
487
|
+
}
|
|
488
|
+
err = me.Err
|
|
489
|
+
}
|
|
490
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
package typesafe
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"testing"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
// Go-specific cases beyond the upstream suite: entries, ordered questions, JSON parsing.
|
|
9
|
+
|
|
10
|
+
func TestEntry_ZeroIsOmittedAndNullIsExplicit(t *testing.T) {
|
|
11
|
+
var zero Entry
|
|
12
|
+
if !zero.IsOmitted() || zero.IsNull() {
|
|
13
|
+
t.Fatal("the zero Entry is omitted, not null")
|
|
14
|
+
}
|
|
15
|
+
if Null.IsOmitted() || !Null.IsNull() {
|
|
16
|
+
t.Fatal("Null is explicit")
|
|
17
|
+
}
|
|
18
|
+
if !EntryOf(nil).IsNull() || Text("x").IsOmitted() || Value(nil).IsOmitted() || !Value(nil).IsNull() {
|
|
19
|
+
t.Fatal("nil converts to null")
|
|
20
|
+
}
|
|
21
|
+
eq(t, EntryOf(Text("t")), Text("t"))
|
|
22
|
+
eq(t, Text("x").Data(), any("x"))
|
|
23
|
+
eq(t, Value(map[string]any{"a": 1}).Data(), any(map[string]any{"a": float64(1)}))
|
|
24
|
+
eq(t, Null.Data(), nil)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func TestEntry_MarshalsJSONValuesAndRejectsNumbersAndBooleans(t *testing.T) {
|
|
28
|
+
for _, tc := range []struct {
|
|
29
|
+
e Entry
|
|
30
|
+
want string
|
|
31
|
+
}{{Text("a\"b"), `"a\"b"`}, {Null, `null`}, {Entry{}, `null`}, {Value([]any{nil, 1}), `[null,1]`}, {Value(map[string]any{"k": true}), `{"k":true}`}, {Value(struct {
|
|
32
|
+
A int `json:"a"`
|
|
33
|
+
}{2}), `{"a":2}`}} {
|
|
34
|
+
raw, err := json.Marshal(tc.e)
|
|
35
|
+
noErr(t, err)
|
|
36
|
+
eq(t, string(raw), tc.want)
|
|
37
|
+
}
|
|
38
|
+
for _, e := range []Entry{Value(1), Value(2.5), Value(true), Value(json.RawMessage(`3`)), Value(make(chan int))} {
|
|
39
|
+
_, err := json.Marshal(e)
|
|
40
|
+
if err == nil {
|
|
41
|
+
t.Fatalf("%v: must not marshal", e)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func TestEntry_ValueKeepsRawMessageKeyOrder(t *testing.T) {
|
|
47
|
+
raw, err := json.Marshal(Noul(json.RawMessage(`{"z":1,"a":{"y":2,"b":3}}`)))
|
|
48
|
+
noErr(t, err)
|
|
49
|
+
eq(t, string(raw), `{"type":"noul","instructions":{"z":1,"a":{"y":2,"b":3}}}`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
func TestEntry_UnmarshalJSON(t *testing.T) {
|
|
53
|
+
var e Entry
|
|
54
|
+
noErr(t, json.Unmarshal([]byte(`null`), &e))
|
|
55
|
+
if !e.IsNull() {
|
|
56
|
+
t.Fatal("null decodes to Null")
|
|
57
|
+
}
|
|
58
|
+
noErr(t, json.Unmarshal([]byte(` "s" `), &e))
|
|
59
|
+
eq(t, e.Data(), any("s"))
|
|
60
|
+
noErr(t, json.Unmarshal([]byte(`{"a":[1]}`), &e))
|
|
61
|
+
eq(t, e.Data(), any(map[string]any{"a": []any{float64(1)}}))
|
|
62
|
+
if err := json.Unmarshal([]byte(`5`), &e); err == nil {
|
|
63
|
+
t.Fatal("a number is not an Entry")
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func TestQuestions_MarshalKeepsOrderAndNames(t *testing.T) {
|
|
68
|
+
raw, err := json.Marshal(Questions{Ask("z", Noul("1")), Ask("a", Noul("2")), Ask("m", Noul("3"))})
|
|
69
|
+
noErr(t, err)
|
|
70
|
+
eq(t, string(raw), `{"z":{"type":"noul","instructions":"1"},"a":{"type":"noul","instructions":"2"},"m":{"type":"noul","instructions":"3"}}`)
|
|
71
|
+
raw, err = json.Marshal(Choice("q", Opt("b", nil), Opt("a", "x")))
|
|
72
|
+
noErr(t, err)
|
|
73
|
+
eq(t, string(raw), `{"type":"choice","instructions":"q","criteria":{"b":null,"a":"x"}}`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
func TestQuestions_ValidateRejectsDuplicatesEmptyAndShortScores(t *testing.T) {
|
|
77
|
+
noErr(t, Questions{Ask("a", Noul("x")), Ask("", Noul("y"))}.Validate())
|
|
78
|
+
err := Questions{Ask("a", Noul("x")), Ask("a", Noul("y"))}.Validate()
|
|
79
|
+
mustAs[*TypeSafeError](t, err)
|
|
80
|
+
contains(t, err.Error(), `Duplicate question name "a"`)
|
|
81
|
+
err = Questions{}.Validate()
|
|
82
|
+
contains(t, err.Error(), "At least one question is required.")
|
|
83
|
+
err = Questions{Ask("s", Score("x", "only"))}.Validate()
|
|
84
|
+
contains(t, err.Error(), `Score question "s" has 1 criteria; at least two scores are required.`)
|
|
85
|
+
err = Questions{Ask("n", nil)}.Validate()
|
|
86
|
+
if err == nil {
|
|
87
|
+
t.Fatal("a nil question must be rejected")
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
func TestParseQuestions_RoundTripsInOrderKeepingOmittedAndNull(t *testing.T) {
|
|
92
|
+
in := `{"zeta":{"type":"noul","criteria":null},"alpha":{"type":"noul","instructions":null,"criteria":{"false":"no"}},` +
|
|
93
|
+
`"c":{"type":"choice","instructions":["a",null],"criteria":{"b":null,"a":{"k":"v"}}},"s":{"type":"score","instructions":"s","criteria":[null,"hi",["x"]]},` +
|
|
94
|
+
`"plain":{"type":"noul"}}`
|
|
95
|
+
qs, err := ParseQuestions([]byte(in))
|
|
96
|
+
noErr(t, err)
|
|
97
|
+
eq(t, len(qs), 5)
|
|
98
|
+
eq(t, qs[0].Name, "zeta")
|
|
99
|
+
eq(t, qs[1].Name, "alpha")
|
|
100
|
+
raw, err := json.Marshal(qs)
|
|
101
|
+
noErr(t, err)
|
|
102
|
+
eq(t, string(raw), in)
|
|
103
|
+
n := qs[4].Question.(NoulQuestion)
|
|
104
|
+
if !n.Instructions.IsOmitted() || n.Criteria != nil {
|
|
105
|
+
t.Fatal("omitted stays omitted")
|
|
106
|
+
}
|
|
107
|
+
z := qs[0].Question.(NoulQuestion)
|
|
108
|
+
if z.Criteria == nil || !z.Criteria.Null {
|
|
109
|
+
t.Fatal("null criteria stays null")
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
func TestParseQuestions_RejectsAChoiceLabelList(t *testing.T) {
|
|
114
|
+
_, err := ParseQuestions([]byte(`{"q":{"type":"choice","instructions":"q","criteria":["a","b"]}}`))
|
|
115
|
+
mustAs[*TypeSafeError](t, err)
|
|
116
|
+
contains(t, err.Error(), "Choice criteria must be a map of labels to descriptions, not a list.")
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
func TestParseQuestions_RejectsAScoreMap(t *testing.T) {
|
|
120
|
+
_, err := ParseQuestions([]byte(`{"q":{"type":"score","criteria":{"0":"bad","1":"good"}}}`))
|
|
121
|
+
mustAs[*TypeSafeError](t, err)
|
|
122
|
+
contains(t, err.Error(), "Score criteria must be a list of descriptions indexed by score from zero, not a map.")
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
func TestParseQuestions_RejectsMalformedInput(t *testing.T) {
|
|
126
|
+
for _, tc := range []struct{ in, want string }{
|
|
127
|
+
{`[]`, "object"},
|
|
128
|
+
{`{"q":{"type":"unknown"}}`, `unknown type "unknown"`},
|
|
129
|
+
{`{"q":{"instructions":"x"}}`, `"q"`},
|
|
130
|
+
{`{"q":5}`, `"q"`},
|
|
131
|
+
{`{"q":{"type":"noul"},"q":{"type":"noul"}}`, `Duplicate question name "q"`},
|
|
132
|
+
{`{"q":{"type":"noul","instructions":5}}`, `"q"`},
|
|
133
|
+
{`{"q":{"type":"choice","criteria":null}}`, `"q"`},
|
|
134
|
+
{`not json`, ""},
|
|
135
|
+
} {
|
|
136
|
+
_, err := ParseQuestions([]byte(tc.in))
|
|
137
|
+
if err == nil {
|
|
138
|
+
t.Fatalf("%s: want an error", tc.in)
|
|
139
|
+
}
|
|
140
|
+
contains(t, err.Error(), tc.want)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
func TestResult_DecodingIsLenientAndAnswersByType(t *testing.T) {
|
|
145
|
+
r := decodeResult(t, `{}`)
|
|
146
|
+
eq(t, len(r.Answers), 0)
|
|
147
|
+
r = decodeResult(t, `{"model":"m","answers":{"x":{"type":"future","v":1}},"usage":{"input_tokens":1,"output_tokens":2},"extra":true}`)
|
|
148
|
+
raw, ok := r.Answers["x"].(RawAnswer)
|
|
149
|
+
if !ok || raw.Type != "future" || string(raw.Raw) != `{"type":"future","v":1}` {
|
|
150
|
+
t.Fatalf("unknown answer types keep their JSON: %#v", r.Answers["x"])
|
|
151
|
+
}
|
|
152
|
+
if _, err := r.Noul("x"); err == nil {
|
|
153
|
+
t.Fatal("a raw answer is not a Noul")
|
|
154
|
+
}
|
|
155
|
+
var bad SystemOneResult
|
|
156
|
+
if err := json.Unmarshal([]byte(`{"answers":{"a":{"type":"noul","noul":"high"}}}`), &bad); err == nil {
|
|
157
|
+
t.Fatal("a mistyped field must fail")
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
func TestResult_MarshalsTheWireForm(t *testing.T) {
|
|
162
|
+
r := decodeResult(t, allAnswersJSON)
|
|
163
|
+
raw, err := json.Marshal(r)
|
|
164
|
+
noErr(t, err)
|
|
165
|
+
jsonEq(t, json.RawMessage(raw), allAnswersJSON)
|
|
166
|
+
}
|