* Add craft-figma-rest: a typed model of the Figma REST file JSON The neutral model for design files: the .fig and .sketch readers map onto it and an application's importer takes it. Typed records for the document tree, paints, effects, text styles, layout grids, constraints, export settings and the component and style metadata; every record keeps the fields it has no name for, so a file read and written loses nothing. The vectorNetwork extension is typed and documented with the others. The layer tree is converted level by level in both directions, and the depth and node limits are checked on the JSON before any typed value is built. * craft-figma-rest: Warnings and style_name for the readers that build this shape Moved from FigCraft's REST importer and .fig reader (crates/format/src/rest.rs Warnings, fig.rs style_name), which the .fig and Sketch mappings share.
craft-figma-rest
A typed model of the Figma REST API file JSON: the response of GET /v1/files/:key (a document
with CANVAS pages, plus components, componentSets and styles) and of
GET /v1/files/:key/nodes. Read and written with serde. No unsafe.
This shape is the neutral model for design files in craft-libs: the .fig reader
(craft-fig) and the .sketch reader (craft-sketch)
map what they read onto it, and an application's importer takes it. It is the file's own shape,
named as the API names it, and knows nothing of any application's document.
Provenance
Written from the published REST API reference only (the documented node types and their properties). No code or schema file of the API's owner or of any other product was copied.
The model
File:name,lastModified,version,schemaVersion,document,components,componentSets,styles.FileNodes/FileNode: thenodesresponse.Node: a layer'sprops(NodeProps, read through the node:node.fills) and itschildren. One record for every node type, since the API's types share most properties.NodePropsnames: identity (id,name,type,visible,locked), geometry (absoluteBoundingBox,absoluteRenderBounds,relativeTransform,size,rotation,constraints), appearance (fills,strokesand the stroke properties,fillGeometry,strokeGeometry,cornerRadius,rectangleCornerRadii,cornerSmoothing,effects,opacity,blendMode,isMask,maskType), auto layout (layoutModeand the padding, spacing, sizing and alignment properties),layoutGrids,exportSettings,styles, text (characters,style,characterStyleOverrides,styleOverrideTable,lineTypes,lineIndentations),componentId,booleanOperation,arcData.Paint,ColorStop,Effect,TypeStyle,LayoutGrid,LayoutConstraint,ExportSetting,Path,ArcData,Color,Vector,Rectangle,Transform, and the metadata recordsComponent,ComponentSet,Style.NodeType,PaintType,EffectType,BlendMode,StyleType: the values known here andOther(String)for the rest. Other enumerations are strings, with their values in the field's documentation.
Every field is optional: the API leaves out what has its default.
Not typed yet
Kept in extra (see below), as JSON: prototyping (interactions, flowStartingPoints,
prototypeDevice, transitions), variables (boundVariables, explicitVariableModes, the local
variables response), component properties (componentPropertyDefinitions,
componentProperties, componentPropertyReferences, overrides) and grid auto layout tracks.
Nothing is lost
Every record has extra: the fields the model has no name for, in the JSON they came in, written
back on output. A file read and written keeps properties the API added after this model was
written. Two things change on a round trip: a field that was null is left out, and a whole
number written as 10 comes back as 10.0.
Extensions
Fields that are not in the REST API, added by the readers that map other files onto this shape so
that nothing those files hold is lost (ext documents each):
| Field | On | Holds |
|---|---|---|
vectorNetwork |
a VECTOR node |
the editable network (ext::VectorNetwork, typed on NodeProps): vertices with corner radii, segments with tangents, regions with loops and winding |
vectorNetworkRef |
a VECTOR node |
an index into a list of networks handed over beside the JSON |
pointCount, innerRadius |
STAR, REGULAR_POLYGON |
points or sides; the star's inner radius as a fraction |
_letterSpacingPercent |
a text style | letter spacing as a percentage of the font size |
A consumer that doesn't know an extension ignores it; the public fields still describe the layer.
Fields starting with _fig are private handoffs between one reader and one importer and are not
part of this contract; they are kept like any unknown field.
For readers
Warnings collects a reader's notes (each once, with a count, at most 200) and style_name
spells a font style the way this shape names it ("SemiBold" → "Semi Bold"), so the readers that
build this shape report and name things alike.
Reading and writing
let file = craft_figma_rest::File::from_slice(&bytes)?; // or from_value(value, max_nodes)
for node in file.document.iter().flat_map(|d| d.walk()) { // parents first, no recursion
if node.kind == Some(craft_figma_rest::NodeType::Text) {
let text = node.characters.as_deref().unwrap_or_default();
}
}
let json = file.to_value()?;
Building one:
use craft_figma_rest::{Node, NodeProps, NodeType};
let rect = Node::new(NodeProps { id: Some("1:1".into()), kind: Some(NodeType::Rectangle), ..Default::default() });
let page = Node::with_children(NodeProps { kind: Some(NodeType::Canvas), ..Default::default() }, vec![rect]);
Limits
Input is hostile. Nothing panics; every limit has a test.
| Limit | Value |
|---|---|
JSON nesting (MAX_DEPTH): about 500 layers inside one another |
1024 levels |
Nodes (from_value takes the limit; from_slice uses DEFAULT_MAX_NODES) |
200 000 |
JSON text nesting (from_slice) |
serde_json's 128 levels |
Both limits are checked on the JSON, without recursion, before any typed value is built.
from_value and to_value take the layer tree apart and put it together level by level, so a
tree at the depth limit is read, written and dropped within a 2 MiB stack (tested). The Serialize
and Deserialize implementations go through serde_json::Value as well, but their serializer
recurses once per level: use to_value / from_value for deep trees.
A field of the wrong type fails the read, and the error names the type expected. A reader that
must skip bad fields one by one (an importer taking files from anywhere) reads the
serde_json::Value instead.