ddonche/goblin-lang
0.46.24
1
0
docs keyword
[[provoke]]

Provoke


Provoke is Goblin's validation mechanism for enforcing constraints and requirements in your code. It comes in three forms: inline statement, block statement, and function call.


Three Forms of Provoke

1. Inline Statement Form

Syntax: provoke => condition

Use for quick, single-condition validation:

act process_file(path)
    provoke => :file_exists(path)
    provoke => :ends_with(path, ".md")
    
    content | :read_text(path)
    return :render(content)
xx
act process_file(path)
    provoke => :file_exists(path)
    provoke => :ends_with(path, ".md")
    
    content | :read_text(path)
    return :render(content)
xx

When it runs:

  • Evaluates the condition
  • If true: continues silently
  • If false: raises an error with the condition shown

Function-Call Form

Goblin also provides a function-call version of Provoke using the intrinsic :provoke.

:provoke(condition)
:provoke(condition, message)

This form behaves exactly like inline provoke => condition, but allows you to supply a custom error message.

Example
:provoke(amount > 0, "Amount must be positive")
Truthiness rules are the same as inline Provoke: nil, 0, empty strings, and empty collections are false; everything else is true.
In the REPL, you must use the : prefix:
:provoke(1 == 1)

2. Block Statement Form

Syntax:

provoke
    condition1
    condition2
    condition3
xx
provoke
    condition1
    condition2
    condition3
xx

Use for multiple related requirements:

act validate_user(username, email, password)
    provoke
        :len(username) >= 3
        :len(username) <= 20
        :starts_with_letter(username)
        :contains(email, "@")
        :len(password) >= 8
        :contains_uppercase(password)
        :contains_number(password)
    xx
    
    return :create_user(username, email, password)
xx
act validate_user(username, email, password)
    provoke
        :len(username) >= 3
        :len(username) <= 20
        :starts_with_letter(username)
        :contains(email, "@")
        :len(password) >= 8
        :contains_uppercase(password)
        :contains_number(password)
    xx
    
    return :create_user(username, email, password)
xx

Behavior:

  • Checks each condition in order
  • Stops at first failure
  • Shows which condition failed in the error message

3. Function Call Form

Syntax: :provoke(condition) or :provoke(condition, message)

Use when you need custom error messages or dynamic validation:

act charge_card(amount, card)
    :provoke(amount > 0, "Amount must be positive")
    :provoke(card !== nil, "Card is required")
    :provoke(card["balance"] >= amount, "Insufficient funds")
    
    :deduct(card, amount)
    return :receipt(amount)
xx
act charge_card(amount, card)
    :provoke(amount > 0, "Amount must be positive")
    :provoke(card !== nil, "Card is required")
    :provoke(card["balance"] >= amount, "Insufficient funds")
    
    :deduct(card, amount)
    return :receipt(amount)
xx

With custom messages:

  • First argument: condition to check
  • Second argument (optional): custom error message
  • Returns true if condition passes
  • Raises error with your message if condition fails

Common Use Cases

Form Validation

act validate_signup_form(form)
    email | form["email"]
    password | form["password"]
    age | form["age"]
    
    provoke
        email !== ""
        :contains(email, "@")
        :len(password) >= 8
        :is_num(age)
        age >= 18
    xx
    
    return { email, password, age }
xx
act validate_signup_form(form)
    email | form["email"]
    password | form["password"]
    age | form["age"]
    
    provoke
        email !== ""
        :contains(email, "@")
        :len(password) >= 8
        :is_num(age)
        age >= 18
    xx
    
    return { email, password, age }
xx

API Input Guards

act create_post(title, body, author_id)
    provoke => title !== ""
    provoke => :len(title) <= 200
    provoke => body !== ""
    provoke => :len(body) <= 50000
    
    :provoke(:user_exists(author_id), "Author not found")
    
    return :save_post(title, body, author_id)
xx
act create_post(title, body, author_id)
    provoke => title !== ""
    provoke => :len(title) <= 200
    provoke => body !== ""
    provoke => :len(body) <= 50000
    
    :provoke(:user_exists(author_id), "Author not found")
    
    return :save_post(title, body, author_id)
xx

File Processing Guards

act process_markdown(path)
    provoke
        :file_exists(path)
        :ends_with(path, ".md")
    xx
    
    content | :read_text(path)
    
    provoke => content !== ""
    provoke => :len(content) < 1000000  /// Max 1MB
    
    return :render_markdown(content)
xx
act process_markdown(path)
    provoke
        :file_exists(path)
        :ends_with(path, ".md")
    xx
    
    content | :read_text(path)
    
    provoke => content !== ""
    provoke => :len(content) < 1000000  /// Max 1MB
    
    return :render_markdown(content)
xx

Data Pipeline Validation

act transform_data(items)
    provoke => :len(items) > 0
    
    cleaned | :map(items, :clean)
    
    provoke
        :len(cleaned) == :len(items)
        :all(cleaned, :is_valid)
    xx
    
    return :process(cleaned)
xx
act transform_data(items)
    provoke => :len(items) > 0
    
    cleaned | :map(items, :clean)
    
    provoke
        :len(cleaned) == :len(items)
        :all(cleaned, :is_valid)
    xx
    
    return :process(cleaned)
xx


Error Messages

Inline Form

x | 15
provoke => x >= 18

/// Error:
/// Provoked constraint violated: x >= 18
///   (x was 15)
x | 15
provoke => x >= 18

/// Error:
/// Provoked constraint violated: x >= 18
///   (x was 15)

Block Form

provoke
    :len(username) >= 3
    :starts_with_letter(username)
xx

/// Error (if 2nd fails):
/// Provoked constraint violated: :starts_with_letter(username)
///   (username was "123abc")
provoke
    :len(username) >= 3
    :starts_with_letter(username)
xx

/// Error (if 2nd fails):
/// Provoked constraint violated: :starts_with_letter(username)
///   (username was "123abc")

Function Form with Custom Message

:provoke(age >= 21, "Must be 21 or older to purchase")

/// Error:
/// Must be 21 or older to purchase
:provoke(age >= 21, "Must be 21 or older to purchase")

/// Error:
/// Must be 21 or older to purchase


Best Practices

1. Front-Load Requirements

Put validation at the top of functions so the business logic isn't buried:

Good:

act process_payment(amount, card)
    provoke
        amount > 0
        card !== nil
        card["active"] == true
    xx
    
    /// Business logic here - clean and clear
    return :charge(card, amount)
xx
act process_payment(amount, card)
    provoke
        amount > 0
        card !== nil
        card["active"] == true
    xx
    
    /// Business logic here - clean and clear
    return :charge(card, amount)
xx

Bad:

act process_payment(amount, card)
    if amount <= 0
        return :error("Invalid amount")
    xx
    if card == nil
        return :error("No card")
    xx
    if card["active"] != true
        return :error("Card inactive")
    xx
    
    /// Business logic buried 12 lines down
    return :charge(card, amount)
xx
act process_payment(amount, card)
    if amount <= 0
        return :error("Invalid amount")
    xx
    if card == nil
        return :error("No card")
    xx
    if card["active"] != true
        return :error("Card inactive")
    xx
    
    /// Business logic buried 12 lines down
    return :charge(card, amount)
xx

2. Use Block Form for Related Checks

Group related validations together:

provoke
    /// Email checks
    email !== ""
    :contains(email, "@")
    :len(email) <= 254
    
    /// Password checks
    :len(password) >= 8
    :contains_uppercase(password)
    :contains_number(password)
xx
provoke
    /// Email checks
    email !== ""
    :contains(email, "@")
    :len(email) <= 254
    
    /// Password checks
    :len(password) >= 8
    :contains_uppercase(password)
    :contains_number(password)
xx

3. Use Custom Messages for User-Facing Errors

:provoke(:can_access(user, resource), "You don't have permission to access this resource")
:provoke(:within_rate_limit(user), "Rate limit exceeded. Please try again later.")
:provoke(:can_access(user, resource), "You don't have permission to access this resource")
:provoke(:within_rate_limit(user), "Rate limit exceeded. Please try again later.")

4. Combine with Guard Clauses

act process_optional_data(data)
    if data == nil => return nil
    
    provoke => :is_valid_format(data)
    
    return :transform(data)
xx
act process_optional_data(data)
    if data == nil => return nil
    
    provoke => :is_valid_format(data)
    
    return :transform(data)
xx


Comparison with Assert

Provoke is for validation - checking inputs, state, and requirements.

Assert (future feature) will be for debugging - checking invariants and assumptions during development.

/// Provoke: Production validation
provoke => amount > 0

/// Assert: Development check (not implemented yet)
:assert(items.len == expected_count, "Internal consistency check")
/// Provoke: Production validation
provoke => amount > 0

/// Assert: Development check (not implemented yet)
:assert(items.len == expected_count, "Internal consistency check")

Use provoke when you want the check to always run in production.


Summary

Form Syntax Use When
Inline provoke => condition Single quick check
Block provoke\n conditions...\nxx Multiple related checks
Function :provoke(condition, msg) Custom error messages

Provoke makes validation declarative, readable, and front-loaded in your functions.