TypeSafeAPI.Question (TypeSafe AI v0.1.0-alpha.3)

Copy Markdown View Source

The three question types and the functions that validate and encode them.

instructions is optional on all three types: the API requires only type for a Noul and type plus criteria for a Choice or a Score. Pass nil (or omit it, for TypeSafeAPI.noul/1) and the key is left out of the request rather than sent as null.

Constructors never validate; validation happens once, locally, when a question set is sent (TypeSafeAPI.evaluate/4), so a Score with one level or a Choice with one option fails fast with a :validation error value instead of a round trip to the API. Only the three known types are accepted; anything else is a local :validation error too.

Questions built at compile time (say, in a module attribute) can be checked eagerly with validate!/1, which raises ArgumentError so the mistake shows up at the line that made it.

questions given to TypeSafeAPI.evaluate/4 are a keyword list, or a list of {id, question} pairs — never a map: question order is the order the model reads them in and the order they travel on the wire, and an Elixir map has no order to preserve. normalize/1 turns the list into an ordered [{id, question}] list, which is the shape the rest of the typed layer works with, and rejects two ids that would collide on the wire (:billing and "billing" are the same JSON key).

Validation is also a promise about encoding: a question that passes validate/1 can always be JSON-encoded. Descriptions and instructions are walked recursively, and anything JSON cannot encode — a struct, a tuple, a keyword list, a pid, a map keyed by something other than a string or an atom — is a :validation error rather than an exception from inside the encoder.

Summary

Types

Anything the API accepts as a description: a string, map, or list.

Questions as the caller passes them: a keyword list or list of pairs.

Instructions for a question. Optional on every type: a nil is left out of the request rather than sent as null.

t()

Functions

Encodes a validated question to its JSON-ready wire map.

Encodes a normalized question list to the wire questions object, preserving the caller's order.

Turns a keyword list (or list of {id, question} pairs) into an ordered [{id, question}] list, validating every id and question and rejecting two ids that share a wire name.

Validates a single question struct.

Validates a question and raises ArgumentError if it is malformed.

Types

description()

@type description() :: String.t() | map() | list()

Anything the API accepts as a description: a string, map, or list.

input()

@type input() :: [{TypeSafeAPI.Keys.key(), t()}]

Questions as the caller passes them: a keyword list or list of pairs.

instructions()

@type instructions() :: description() | nil

Instructions for a question. Optional on every type: a nil is left out of the request rather than sent as null.

t()

Functions

encode(question)

@spec encode(t()) :: map()

Encodes a validated question to its JSON-ready wire map.

encode_all(questions)

@spec encode_all([{TypeSafeAPI.Keys.key(), t()}]) :: TypeSafeAPI.JSON.Encoded.t()

Encodes a normalized question list to the wire questions object, preserving the caller's order.

The result is a TypeSafeAPI.JSON.Encoded: the object is serialized to JSON bytes here, once, so a question set reused across many states (TypeSafeAPI.evaluate_many/4) is not re-serialized per request. The TypeSafeAPI.JSON.OrderedObject it was built from is still available as encoded.object.

normalize(questions)

@spec normalize(input()) ::
  {:ok, [{TypeSafeAPI.Keys.key(), t()}]} | {:error, TypeSafeAPI.Error.t()}

Turns a keyword list (or list of {id, question} pairs) into an ordered [{id, question}] list, validating every id and question and rejecting two ids that share a wire name.

Maps are not accepted: they have no order, and question order is what the model sees. Pass a keyword list instead.

validate(question)

@spec validate(term()) :: :ok | {:error, String.t()}

Validates a single question struct.

validate!(question)

@spec validate!(t()) :: t()

Validates a question and raises ArgumentError if it is malformed.

Returns the question unchanged, so it can wrap a constructor:

@urgent TypeSafeAPI.Question.validate!(TypeSafeAPI.noul("Urgent?", true: "time-sensitive"))