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:
objis the object to test again. It is aLocatedValueobject, which is a wrapper around the value that also contains information about where the value comes from.contextcontains information about the requested analysis.
The return value is a tuple containing two elements:
- The
DiagnosticAnalysisinstance 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
EllipsisTypeif 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 anintor a string parseable as an integer. Produces anintinstance. -
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 QuantopsQuantityinstance. Produces aQuantityinstance. -
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 aPathFileRefinstance.bytes– Binary data (only in binary mode). Produces aBytesIOFileRefinstance.io.IOBase– A file-like object, such as one returned byopen(). Produces anIOBaseFileRefinstance.
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