Implementing input validation and transformation
Introduction
Automancer places a strong emphasis on input validation, and in particular on performing validation as soon as possible. Its robust validation system is designed to catch as many errors as soon as possible, even when the input is a dynamic expression that cannot be evaluated until runtime. Input validation also provides advanced language features which improve user experience in the editor.
Plugins can integrate with the validation and transformation system to bring all of these features to new processes and programs. This guide will cover the basics of this topic.
Working with types
All objects that define a type must have an analyze()
method with the following signature:
from types import EllipsisType
import automancer as am
import snaptext
class SomeType:
def analyze(self, obj: snaptext.LocatedValue, /, context: am.AnalysisContext) -> tuple[am.DiagnosticAnalysis, Any | EllipsisType]:
...
For convenience, this signature is replicated in the Type
protocol.
Two parameters are passed to this method:
obj
is the object to test again. It is aLocatedValue
object, which is a wrapper around the value that also contains information about where the value comes from.context
contains information about the requested analysis.
The return value is a tuple containing two elements:
- The
DiagnosticAnalysis
instance contains errors and warnings. If it is instead aLanguageServiceAnalysis
, a subclass ofDiagnosticAnalysis
, it can also carry details required to implement certain language features, such as completion, hover and fold ranges. - The second element is the transformed object, or
EllipsisType
if the analysis failed.
The success of the analysis is determined only by the second element of the tuple, and not by the presence of errors or warnings in the LanguageServiceAnalysis
instance. The analysis may be successful even if there are errors or warnings.
To create Ellipsis
instance or check whether a value is an Ellipsis
instance, use the following:
from types import EllipsisType
x = Ellipsis
x = ...
success = not isinstance(x, EllipsisType)
success = x is not Ellipsis # Also works but not detected by type checkers
Using built-in types
Built-in types should cover most use cases. They are:
-
AnyType
– Any value. -
RecordType
– A dictionary with a fixed set of string keys, with values of different types. -
ListType
– A list of values of the same type. -
StrType
– A string. -
IntType
– An integer. Accepts either anint
or a string parseable as an integer. Produces anint
instance. -
QuantityType
– A quantity with a magnitude and a unit. Accepts either a string parseable as a quantity, an integer or float assumed to be dimensionless, or a QuantopsQuantity
instance. Produces aQuantity
instance. -
KVDictType
– A dictionary with keys of the same type and values of the same type. -
ReadableDataRefType
– A reference to a readable data source, in binary or text mode. Accepts one of the following:os.PathLike
(includingpathlib.Path
) orstr
– A path to a file. Produces aPathFileRef
instance.bytes
– Binary data (only in binary mode). Produces aBytesIOFileRef
instance.io.IOBase
– A file-like object, such as one returned byopen()
. Produces anIOBaseFileRef
instance.
Produces a subclass of
FileRef
. See Working with data objects for details.
Creating record types
The record type is an ubiquitous type that represents a dictionary with a fixed set of string keys, with values of different types. It is used to represent configuration, protocols, and other data structures.
The values of the record can either be types themselves or Attribute
instances. The latter is used to provide additional information about the attribute, such as a description or a label. These are visible in the editor during completion and when hovering over the attribute.
The following example shows a possible record type definition for a plugin that implements uploading data to an FTP server:
am.RecordType({
'address': am.Attribute(
am.StrType(),
description="The address of the FTP server",
label="Address"
),
'destination': am.StrType(),
'source': am.Attribute(
am.ReadableDataRefType(),
description="The source object to upload",
label="Source object"
)
})
In a protocol, the type could be used to validate the following:
ftp-upload:
address: ftp.example.com
destination: path/to/destination.txt
source: path/to/source.txt