Showing someone what a configuration holds is easiest in the syntax they
wrote it in. xcl.EncodeEntity turns one entity back into configuration text,
and the Config’s EncodeSavedEntity does the same from its saved data. In
Python the two are cfg.encode_entity and cfg.encode_saved_entity, and the
engine renders the text, so Python and Go write identical text for the same
entity. Both write exactly one block, so convert several entities by calling
once for each. Either can colour the text it writes, for a terminal or any
other format.
Converting an entity
Pass an entity as the configuration holds it, as FindResource, Entities and
FindEntitiesByType return it. A copy Find[T] returns is your own plain
struct, which does not say what it was declared as, and is refused:
db, err := c.FindResource("database.postgres.main")if err != nil { return err}
text, err := xcl.EncodeEntity(db)if err != nil { return err}
fmt.Println(string(text))db = cfg.find(PostgreSQL, "database.postgres.main")
print(cfg.encode_entity(db))With xcl.aio.Config the call is await cfg.encode_entity(db). Any entity a lookup returns works, as does an instance of a registered class you built yourself, and the xcl.Entity a lookup returns for a plugin’s type. A field you change on a looked-up entity shows in the text.
database "postgres" "main" { location = "localhost" port = 5432
timeouts { connection = 10 keep_alive = 60 }}The text shows what a person wrote. xcl’s own bookkeeping is not written, and
neither is depends_on. A variable, output or module is never written as
configuration, and asking for one fails with xcl.ErrNotEncodable.
Converting saved data
c.EncodeSavedEntity takes an entity’s saved data, in the form state stores it
and events carry it at xcl.EventDataProcessed. The record’s type is resolved
through the Config, from the Go types its registries declare and the types
its plugins provide, and its plugins are loaded if they are not loaded already:
text, err := c.EncodeSavedEntity(event.Data)text = cfg.encode_saved_entity(event.data)It takes the same keyword choices as cfg.encode_entity, such as cfg.encode_saved_entity(event.data, include_computed=True). Here event is the xcl.Event an on_event handler receives, and event.data holds the saved record when the Config was created with event_data="processed". With xcl.aio.Config the call is await cfg.encode_saved_entity(event.data).
The text is identical to what EncodeEntity returns for the same entity. A
record naming a type the Config does not know fails with
xcl.ErrUnregisteredType, data that is not one saved record with
xcl.ErrInvalidSavedData, and a plugin that fails to load with
xcl.ErrPluginLoad.
An event receiver does not need the Config at all. Event.Entity() returns
the entity an event carries, as the Go type registered for its block type and
with every sensitive value masked, ready for EncodeEntity. It returns nil
when the event carries no data:
entity, err := e.Entity()if err != nil || entity == nil { return}
text, err := xcl.EncodeEntity(entity)entity = event.entity()if entity is None: return
text = cfg.encode_entity(entity)An event entity kept and encoded after its operation has finished shows the engine’s current value with reveal_sensitive=True, where Go’s event data always shows the marker.
Provider-filled values
Values a provider filled in, such as an address or an id it assigned, are left
out by default. Ask for them with xcl.IncludeComputed(), and each one is
marked so a reader can tell it apart:
text, err := xcl.EncodeEntity(db, xcl.IncludeComputed())text = cfg.encode_entity(db, include_computed=True)database "postgres" "main" { location = "localhost" port = 5432 connection_string = "postgres://admin@localhost:5432/main" # set by the provider}Empty attributes
An optional attribute left unset, or set to its zero value, is left out by
default. Ask for xcl.IncludeEmpty() and each one is written with the empty
value of its type: "" for a string, [] for a list, {} for a map, and
the zero value of the type a nullable attribute holds:
text, err := xcl.EncodeEntity(db, xcl.IncludeEmpty())text = cfg.encode_entity(db, include_empty=True)Sensitive values
A sensitive value is written as the marker "(sensitive)". Pass
xcl.RevealSensitive() to write the real value. Event data carries the marker
rather than the value, so text made from event data shows the marker even when
revealing is asked for. See Sensitive values for
declaring a field sensitive.
text, err := xcl.EncodeEntity(db, xcl.RevealSensitive())text = cfg.encode_entity(db, reveal_sensitive=True)A looked-up entity holds only the marker, so the engine writes its own configured value. A value the entity holds for real, such as one you changed or one set on an instance you built yourself, is written as it is. An event entity kept until after its operation shows the engine’s current value, unlike Go’s event data.
Dependency lists
A depends_on list is written exactly as you wrote it, and left out when you
wrote none. The dependencies xcl works out from references are never added to
it, though they still order creation and destruction. Given this
configuration:
app "c" { y = "blue"}
app "a" { depends_on = ["app.b"]
x = app.c.y}the text for app.a names only the dependency you wrote, while the
reference to app.c comes out as the value it resolved to:
app "a" { depends_on = ["app.b"]
x = "blue"}The list is the same after the configuration is applied and when it is loaded back from saved state, so text from a live entity and from its saved data matches.
Showing references as written
By default a field that referred to another entity shows the value it resolved
to. Ask for xcl.ShowReferences() and each such field is written exactly as the
user wrote it instead, whether a bare reference, a template or a reference in a
nested block. Given this configuration:
app "web" { db_location = database.postgres.main.location url = "https://${database.postgres.main.location}/app"}text, err := xcl.EncodeEntity(app, xcl.ShowReferences())text = cfg.encode_entity(app, show_references=True)An instance you built yourself has no references, so its values are written as they are.
the text shows each reference as it was written:
app "web" { db_location = database.postgres.main.location url = "https://${database.postgres.main.location}/app"}Without the option the same entity is written with its resolved values:
app "web" { db_location = "localhost" url = "https://localhost/app"}It works the same from saved data,
c.EncodeSavedEntity(record, xcl.ShowReferences()), and the two
give byte-identical text, since the references are kept in each entity’s saved
record. It combines with xcl.IncludeComputed().
A sensitive field shows its reference only when it was written as a single bare
reference, such as password = variable.db_password, since an address holds no
secret. Written any other way it keeps "(sensitive)", unless
xcl.RevealSensitive() is also given. State saved by an earlier version of xcl
holds no references, so its text shows resolved values until the configuration
is applied again.
Highlighting
Configuration text is always plain. Highlighting it is a second step:
highlight.Pieces splits the text into pieces, each labelled with the same
name the xcl VS Code extension gives it, and a renderer turns the pieces into
output, so the configuration in your terminal or your page looks the way it
does in the editor. It works on the text every option produces, including
xcl.ShowReferences(), whose references are coloured like any other text.
In a terminal
xcl ships a terminal renderer in the highlight package, highlight.ANSI. It
uses only the terminal’s 16 standard colours, so the result follows the
user’s own palette:
import "github.com/jumppad-labs/xcl/highlight"
text, err := xcl.EncodeEntity(entity)if err != nil { return err}
os.Stdout.Write(highlight.ANSI.Render(highlight.Pieces(text)))print(cfg.encode_entity(entity, highlight=True))highlight=True uses the terminal renderer with no theme, and cfg.encode_saved_entity(event.data, highlight=True) colours a saved record the same way. It combines with every other choice.
xcl never checks whether output is a terminal. Whether to colour is your
decision, so colour only when the output shows colour, and render with
highlight.Text, which gives back the plain text, otherwise:
var renderer highlight.Renderer = highlight.Textif isTerminal(os.Stdout) { renderer = highlight.ANSI}
os.Stdout.Write(renderer.Render(highlight.Pieces(text)))import sys
text = cfg.encode_entity(entity, highlight=sys.stdout.isatty())Here isTerminal is your application’s own check. The
logging example
makes the same decision for its writer before asking for colour. Removing the
colour codes from highlighted text gives back exactly the plain text, and each
line is coloured on its own, so the text can be indented or prefixed line by
line without colour leaking.
With an editor theme
Give the renderer a VS Code colour theme and it colours tokens the way the editor does with that theme, in 24-bit colour, with the bold, italic, underline and strikethrough styles the theme sets:
renderer, err := highlight.NewANSI(highlight.WithThemeFile("dark-plus.json"))if errors.Is(err, xcl.ErrInvalidTheme) { var detail *xcl.InvalidThemeError errors.As(err, &detail) log.Fatalf("theme %s: %s", detail.Source, detail.Reason)}Editor themes are not yet available from Python; cfg.encode_entity and cfg.encode_saved_entity offer highlight=True only, with the terminal’s 16 standard colours. See the parity list for what the package offers.
highlight.WithTheme reads the theme from an io.Reader instead. The theme
is the JSON file a VS Code theme extension ships, and comments and trailing
commas are allowed, as the editor allows them. Only its tokenColors are
used, and a token the theme has no rule for keeps the terminal’s default
colour. A theme that cannot be read, or is not valid, fails when the renderer
is created with xcl.ErrInvalidTheme, rather than quietly falling back to
the default colours. A theme that relies on include is rejected the same
way, since it does not hold its own tokenColors.
Writing a renderer
A renderer has one method, Render(pieces []highlight.Piece) []byte, and is
given every piece of the text at once, in order, so it can reason across
them, such as by line. Each highlight.Piece has a Scope, the TextMate
scope the xcl-vscode grammar gives it, such as storage.type.xcl for a block
type or variable.other.property.xcl for an attribute name, and its Text.
Text the grammar leaves unlabelled, such as whitespace, braces and commas, is a
piece too, with an empty scope, so a renderer for a format that needs escaping
sees every byte. highlight.RendererFunc turns a function over all the pieces
into a renderer, and highlight.PieceFunc one that styles each piece on its
own, joining what it returns:
renderer := highlight.PieceFunc(func(piece highlight.Piece) string { text := html.EscapeString(piece.Text) if piece.Scope == "" { return text }
class := strings.ReplaceAll(strings.TrimSuffix(piece.Scope, ".xcl"), ".", "-") return `<span class="` + class + `">` + text + `</span>`})
page := renderer.Render(highlight.Pieces(text))Writing a renderer is not yet available from Python; cfg.encode_entity and cfg.encode_saved_entity offer highlight=True only. See the parity list for what the package offers.
Every scope is exported as a string constant named highlight.Scope..., such as
highlight.ScopeAttribute and highlight.ScopeReference, so a renderer can
compare against them, or match a family by prefix, such as every scope
starting with comment.. highlight.Pieces labels any configuration text you
already hold, not only text encoded from an entity.
For reading, not reprocessing
Configuration text is for people to read. By default references come out as
the literal values they resolved to, unless you ask for ShowReferences.
Comments and layout from the original file are not kept, and text including
provider-filled values does not validate, since xcl refuses a configuration
that sets them.