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.
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
Anything the API accepts as a description: a string, map, or list.
@type input() :: [{TypeSafeAPI.Keys.key(), t()}]
Questions as the caller passes them: a keyword list or list of pairs.
@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.
@type t() :: TypeSafeAPI.Question.Noul.t() | TypeSafeAPI.Question.Choice.t() | TypeSafeAPI.Question.Score.t()
Functions
Encodes a validated question to its JSON-ready wire map.
@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.
@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.
Validates a single question struct.
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"))