Shapes
Shapes are Sentrie’s primary mechanism for defining custom data models and type aliases. They provide a powerful way to create reusable, validated types that can be composed together to build complex data models.
Shapes help in providing clear contracts for data, thus providing a built in validation layer for data that flows through your policies.
What are Shapes?
Section titled “What are Shapes?”Shapes serve two main purposes in Sentrie:
- Data Models: Define structured objects with named fields and constraints
- Type Aliases: Create custom names for existing types with additional constraints
Defining Shapes
Section titled “Defining Shapes”Type Aliases
Section titled “Type Aliases”Shapes can be used to create type aliases for built-in types with additional constraints:
shape ID string @uuid()shape Email string @email()shape Username string @minlength(3) @maxlength(20) @alphanumeric()shape UserAge number @min(13) @max(120)This creates constrained types that can be used throughout your policies:
let id: ID = "123e4567-e89b-12d3-a456-426614174000" -- Validlet email: Email = "alice@example.com" -- Validlet username: Username = "alice123" -- Validlet age: UserAge = 25 -- Validlet invalid_username: Username = "ab" -- Validation error: too shortSimple Data Models
Section titled “Simple Data Models”The most common use of shapes is to define structured data models:
shape User { name: string age: number email: string}This creates a User shape with three fields. You can then use this shape to create and validate user data:
let user: User = { name: "Alice", age: 28, email: "alice@example.com"}Field Nullability and Optionality
Section titled “Field Nullability and Optionality”Shapes support different field requirements using special markers:
- Required fields (default): Field must be present and can be null
- Required non-null fields (
!): Field must be present and cannot be null - Optional fields (
?): Field may be omitted entirely
shape User { name!: string -- Required, cannot be null age: number -- Required, can be null email?: string -- Optional, can be omitted phone!?: string -- Optional, but if present cannot be null}Examples of valid and invalid usage:
-- Valid: all required fields present, optional field omittedlet user1: User = { name: "Alice", age: 25}
-- Valid: all fields present, including optionallet user2: User = { name: "Bob", age: 30, email: "bob@example.com"}
-- Valid: required field can be nulllet user3: User = { name: "Charlie", age: null}
-- Invalid: required non-null field is nulllet user4: User = { name: null, -- Error: name cannot be null age: 25}
-- Invalid: required field is missinglet user5: User = { age: 25 -- Error: name is required}Shape Composition
Section titled “Shape Composition”Shapes can be composed from other shapes using the with keyword, allowing you to build complex data models.
Basic Composition
Section titled “Basic Composition”shape User { name: string age: number}
shape AdminUser with User { permissions: list[string] department: string}The AdminUser shape includes all fields from User and adds additional fields:
let admin: AdminUser = { name: "John", age: 30, permissions: ["read", "write", "delete"], department: "Engineering"}Composition Rules
Section titled “Composition Rules”- Composition: Composed shapes include all fields from base shapes
- No Circular Dependencies: Shapes cannot reference themselves directly or indirectly
- No Duplicate Fields: Shapes cannot have duplicate fields directly or indirectly
- Type Safety: Values must match the exact shape they’re assigned to
- Data Model Requirement: Shapes cannot be composed with alias shapes
-- This will cause a validation errorlet user: User = { name: "John", age: 30, permissions: ["read", "write"] -- User doesn't have permissions field}Applying Constraints
Section titled “Applying Constraints”Shape properties can have constraints applied to them, providing runtime validation:
shape User { name: string @minlength(2) @maxlength(50) @not_empty() age: number @min(0) @max(150) email: string @email() @not_empty() score: number @range(0.0, 100.0)}When creating instances of this shape, all constraints are validated:
let user: User = { name: "Alice", -- Valid: meets length requirements age: 25, -- Valid: within range email: "alice@test.com", -- Valid: proper email format score: 85.5 -- Valid: within range}
-- This would fail validationlet invalid_user: User = { name: "A", -- Too short age: -5, -- Below minimum email: "invalid-email", -- Not a valid email score: 150.0 -- Above maximum}Nested Data Models
Section titled “Nested Data Models”Shapes excel at modeling complex, nested data models with proper nullability handling:
shape Address { street!: string @not_empty() city!: string @not_empty() state!: string @length(2) @uppercase() zip!: string @regexp("^[0-9]{5}(-[0-9]{4})?$")}
shape ContactInfo { email!: string @email() phone?: string @regexp("^\\+?[1-9]\\d{1,14}$") -- Optional phone address?: Address -- Optional address}
shape User { name!: string @minlength(2) @maxlength(100) age: number @min(13) @max(120) -- Can be null if unknown contact!: ContactInfo preferences?: map[string] -- Optional preferences}Usage examples showing different nullability scenarios:
-- Complete user with all fieldslet user1: User = { name: "John Doe", age: 30, contact: { email: "john@example.com", phone: "+1234567890", address: { street: "123 Main St", city: "Anytown", state: "CA", zip: "12345" } }, preferences: { "theme": "dark", "notifications": "enabled" }}
-- User with minimal required fields (optional fields omitted)let user2: User = { name: "Jane Smith", age: null, -- Age unknown contact: { email: "jane@example.com" -- phone and address are optional, so omitted } -- preferences are optional, so omitted}
-- User with some optional fieldslet user3: User = { name: "Bob Wilson", age: 25, contact: { email: "bob@example.com", phone: "+1987654321" -- address omitted (optional) }, preferences: { "theme": "light" }}Shape Management
Section titled “Shape Management”Local Availability
Section titled “Local Availability”Shapes are automatically available within their defining namespace:
namespace com/example/users
shape User { name: string age: number}
policy example { -- User shape is available here let user: User = { name: "Alice", age: 25 }}Exporting Shapes
Section titled “Exporting Shapes”To make shapes available to other namespaces, use the export keyword:
namespace com/example/auth
shape User { id: string @uuid() username: string @minlength(3) email: string @email()}
export shape UserImporting Shapes
Section titled “Importing Shapes”Other namespaces can import and use exported shapes by using the fully qualified name:
namespace com/example/billing
policy process_payment { -- Import the User shape using the fully qualified name from auth namespace shape User with com/example/auth/User { billing_address: string }
let user: User = { id: "123e4567-e89b-12d3-a456-426614174000", username: "alice", email: "alice@example.com", billing_address: "456 Oak Ave" }}Policy Local Shapes
Section titled “Policy Local Shapes”Shapes defined in a policy are only available within that policy and take precedence over namespace shapes:
namespace com/example/billing
shape User { id!: string @uuid() email!: string @email()}
policy process_payment_1 { -- Policy-local shape (most specific) shape User { id!: string @uuid() billing_address!: string }
-- This resolves to the policy-local `User` shape let user: User = { id: "123e4567-e89b-12d3-a456-426614174000", billing_address: "123 Main St" }
...}
policy process_payment_2 { -- This resolves to the namespace `User` shape let user: User = { id: "123e4567-e89b-12d3-a456-426614174000", billing_address: "123 Main St" }
...}Best Practices
Section titled “Best Practices”Naming Conventions
Section titled “Naming Conventions”- Use PascalCase for shape names:
UserProfile,PaymentInfo - Use descriptive names that clearly indicate the shape’s purpose
- Avoid generic names like
DataorInfounless they’re truly generic
Organization
Section titled “Organization”- Group related shapes in the same namespace
- Export only shapes that need to be used by other namespaces
- Use composition to avoid duplicating common fields
Best Practices
Section titled “Best Practices”- Apply appropriate constraints to shape fields
- Use meaningful constraint messages for better error reporting
- Use alias shapes for better readability
shape Category string @one_of("electronics", "clothing", "books", "home")shape Product { name!: string @minlength(1) @maxlength(100) @not_empty() price!: number @positive() @range(0.01, 999999.99) category!: Category in_stock: bool}