1package typesafe23import (4 "bytes"5 "encoding/json/jsontext"6 jsonv2 "encoding/json/v2"7 "fmt"8)910// API limits enforced by the hosted service.11const (12 // MaxChoiceOptions is the maximum number of labels in a ChoiceQuestion.13 MaxChoiceOptions = 25514 // MaxScoreLevels is the maximum number of levels in a ScoreQuestion.15 MaxScoreLevels = 1016)1718// Question is a typed System One question.19//20// The concrete question types are NoulQuestion, ChoiceQuestion, and21// ScoreQuestion. Question values always include their wire type discriminator.22type Question interface {23 questionType() string24 jsonv2.Marshaler25}2627// NoulCriteria describes the true and false outcomes of a Noul question.28type NoulCriteria struct {29 True any `json:"true"`30 False any `json:"false"`31}3233// NoulQuestion asks for the probability of a yes answer. It requires34// Instructions, a non-null criterion, or both.35type NoulQuestion struct {36 Instructions any `json:"instructions"`37 Criteria *NoulCriteria `json:"criteria"`38}3940func (NoulQuestion) questionType() string { return "noul" }4142// ChoiceQuestion selects one label from Criteria, which must contain between43// 2 and MaxChoiceOptions labels. A label's description may be nil.44type ChoiceQuestion struct {45 Instructions any `json:"instructions"`46 Criteria map[string]any `json:"criteria"`47}4849func (ChoiceQuestion) questionType() string { return "choice" }5051// ScoreQuestion estimates a score using ordered rubric levels in Criteria,52// which must contain between 2 and MaxScoreLevels non-null levels.53type ScoreQuestion struct {54 Instructions any `json:"instructions"`55 Criteria []any `json:"criteria"`56}5758func (ScoreQuestion) questionType() string { return "score" }5960func marshalQuestion(kind string, value any) ([]byte, error) {61 payload, err := jsonv2.Marshal(value)62 if err != nil {63 return nil, err64 }65 var object map[string]jsontext.Value66 if err := jsonv2.Unmarshal(payload, &object); err != nil {67 return nil, fmt.Errorf("marshal %s question: %w", kind, err)68 }69 object["type"] = jsontext.Value(fmt.Sprintf("%q", kind))70 return jsonv2.Marshal(object)71}7273// MarshalJSON encodes a NoulQuestion with its discriminator.74func (q NoulQuestion) MarshalJSON() ([]byte, error) {75 return marshalQuestion(q.questionType(), struct {76 Instructions any `json:"instructions"`77 Criteria *NoulCriteria `json:"criteria"`78 }{q.Instructions, q.Criteria})79}8081// MarshalJSON encodes a ChoiceQuestion with its discriminator.82func (q ChoiceQuestion) MarshalJSON() ([]byte, error) {83 return marshalQuestion(q.questionType(), struct {84 Instructions any `json:"instructions"`85 Criteria map[string]any `json:"criteria"`86 }{q.Instructions, q.Criteria})87}8889// MarshalJSON encodes a ScoreQuestion with its discriminator.90func (q ScoreQuestion) MarshalJSON() ([]byte, error) {91 return marshalQuestion(q.questionType(), struct {92 Instructions any `json:"instructions"`93 Criteria []any `json:"criteria"`94 }{q.Instructions, q.Criteria})95}9697// UnmarshalQuestion decodes a discriminated question.98func UnmarshalQuestion(data []byte) (Question, error) {99 var envelope struct {100 Type string `json:"type"`101 }102 if err := jsonv2.Unmarshal(data, &envelope); err != nil {103 return nil, fmt.Errorf("decode question discriminator: %w", err)104 }105 switch envelope.Type {106 case "noul":107 var value NoulQuestion108 if err := jsonv2.Unmarshal(data, &value); err != nil {109 return nil, err110 }111 return value, nil112 case "choice":113 var value ChoiceQuestion114 if err := jsonv2.Unmarshal(data, &value); err != nil {115 return nil, err116 }117 return value, nil118 case "score":119 var value ScoreQuestion120 if err := jsonv2.Unmarshal(data, &value); err != nil {121 return nil, err122 }123 return value, nil124 default:125 return nil, fmt.Errorf("unknown question type %q", envelope.Type)126 }127}128129func validateQuestion(name string, question Question) error {130 if question == nil {131 return fmt.Errorf("question %q is nil", name)132 }133 switch value := question.(type) {134 case *NoulQuestion:135 if value == nil {136 return fmt.Errorf("question %q is typed nil", name)137 }138 return validateQuestion(name, *value)139 case *ChoiceQuestion:140 if value == nil {141 return fmt.Errorf("question %q is typed nil", name)142 }143 return validateQuestion(name, *value)144 case *ScoreQuestion:145 if value == nil {146 return fmt.Errorf("question %q is typed nil", name)147 }148 return validateQuestion(name, *value)149 }150 return validateQuestionValue(name, question)151}152153func validateQuestionValue(name string, question Question) error {154 switch value := question.(type) {155 case NoulQuestion:156 kind, err := topLevelKind(value.Instructions)157 if err != nil {158 return fmt.Errorf("question %q instructions: %w", name, err)159 }160 described := kind != 'n'161 if value.Criteria != nil {162 kind, err := topLevelKind(value.Criteria.True)163 if err != nil {164 return fmt.Errorf("question %q criteria.true: %w", name, err)165 }166 described = described || kind != 'n'167 if kind, err = topLevelKind(value.Criteria.False); err != nil {168 return fmt.Errorf("question %q criteria.false: %w", name, err)169 }170 described = described || kind != 'n'171 }172 if !described {173 return fmt.Errorf("question %q noul requires instructions or criteria", name)174 }175 case ChoiceQuestion:176 if len(value.Criteria) < 2 {177 return fmt.Errorf("question %q choice criteria must contain at least two labels", name)178 }179 if len(value.Criteria) > MaxChoiceOptions {180 return fmt.Errorf("question %q choice criteria must contain at most %d labels", name, MaxChoiceOptions)181 }182 if err := validateTopLevelValue(value.Instructions); err != nil {183 return fmt.Errorf("question %q instructions: %w", name, err)184 }185 for label, criterion := range value.Criteria {186 if err := validateTopLevelValue(criterion); err != nil {187 return fmt.Errorf("question %q criteria[%q]: %w", name, label, err)188 }189 }190 case ScoreQuestion:191 if len(value.Criteria) < 2 {192 return fmt.Errorf("question %q score criteria must contain at least two levels", name)193 }194 if len(value.Criteria) > MaxScoreLevels {195 return fmt.Errorf("question %q score criteria must contain at most %d levels", name, MaxScoreLevels)196 }197 if err := validateTopLevelValue(value.Instructions); err != nil {198 return fmt.Errorf("question %q instructions: %w", name, err)199 }200 for index, criterion := range value.Criteria {201 if kind, err := topLevelKind(criterion); err != nil {202 return fmt.Errorf("question %q criteria[%d]: %w", name, index, err)203 } else if kind == 'n' {204 return fmt.Errorf("question %q criteria[%d]: score level must not be null", name, index)205 }206 }207 default:208 return fmt.Errorf("question %q has unsupported type %T", name, question)209 }210 return nil211}212213func rawObject(data []byte) (map[string]jsontext.Value, error) {214 var object map[string]jsontext.Value215 if err := jsonv2.Unmarshal(data, &object); err != nil {216 return nil, err217 }218 if object == nil {219 return nil, fmt.Errorf("expected JSON object")220 }221 return object, nil222}223224func hasRaw(object map[string]jsontext.Value, key string) bool {225 value, ok := object[key]226 return ok && !bytes.Equal(bytes.TrimSpace(value), []byte("null"))227}