{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://docs.smithable.ai/schema/smithable-0.1.json",
  "title": "smithable.yml",
  "description": "Describes what a Smithable application does. See docs/spec.",
  "type": "object",
  "required": [
    "smithable",
    "app"
  ],
  "additionalProperties": false,
  "properties": {
    "smithable": {
      "description": "The spec format version.",
      "enum": [
        0.1,
        "0.1"
      ]
    },
    "app": {
      "type": "object",
      "description": "The application as a whole.",
      "required": [
        "name"
      ],
      "additionalProperties": false,
      "properties": {
        "name": {
          "type": "string",
          "minLength": 1,
          "description": "The application's name."
        },
        "currency": {
          "type": "string",
          "pattern": "^[A-Z]{3}$",
          "description": "ISO 4217 currency code, e.g. EUR."
        },
        "timezone": {
          "type": "string",
          "description": "IANA time zone, e.g. Europe/Mariehamn."
        },
        "description": {
          "type": "string",
          "description": "What this is. Meaning for AI and developers; not shown to visitors."
        },
        "intent": {
          "type": "string",
          "description": "The outcome or experience this should achieve."
        },
        "rules": {
          "type": "array",
          "description": "Domain rules in plain language. A rule does nothing by itself until something implements it.",
          "items": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "object",
                "required": [
                  "rule"
                ],
                "additionalProperties": false,
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "rule": {
                    "type": "string"
                  }
                }
              }
            ]
          }
        }
      }
    },
    "models": {
      "type": "object",
      "description": "The data the application stores. Model names are PascalCase.",
      "propertyNames": {
        "pattern": "^[A-Z][A-Za-z0-9]*$"
      },
      "additionalProperties": {
        "type": "object",
        "required": [
          "fields"
        ],
        "additionalProperties": false,
        "properties": {
          "fields": {
            "type": "object",
            "minProperties": 1,
            "propertyNames": {
              "pattern": "^[a-z][A-Za-z0-9]*$",
              "not": {
                "enum": [
                  "id",
                  "owner",
                  "createdAt",
                  "updatedAt"
                ]
              }
            },
            "additionalProperties": {
              "oneOf": [
                {
                  "type": "string",
                  "pattern": "^\\s*(?:(?:string|text|markdown|int|decimal|money|bool|date|datetime|email|phone|url|picture|pictures)!?(?:\\s+unique)?(?:\\s*=\\s*\\S.*)?|[A-Z][A-Za-z0-9]*!?(?:\\s+unique)?|[A-Z][A-Za-z0-9]*\\[\\]|[a-z][A-Za-z0-9]*(?:\\s*\\|\\s*[a-z][A-Za-z0-9]*)+)\\s*$",
                  "description": "A type with optional \"!\" (required), \"unique\" and \"= default\", e.g. \"string!\" or \"int! = 60\". Or choices: \"draft | sent | paid\" (the first is the default)."
                },
                {
                  "type": "object",
                  "required": [
                    "type"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "type": {
                      "type": "string",
                      "pattern": "^\\s*(?:(?:string|text|markdown|int|decimal|money|bool|date|datetime|email|phone|url|picture|pictures)!?|[A-Z][A-Za-z0-9]*!?|[A-Z][A-Za-z0-9]*\\[\\]|[a-z][A-Za-z0-9]*(?:\\s*\\|\\s*[a-z][A-Za-z0-9]*)+)\\s*$"
                    },
                    "required": {
                      "type": "boolean"
                    },
                    "unique": {
                      "type": "boolean"
                    },
                    "default": {
                      "type": [
                        "string",
                        "number",
                        "boolean"
                      ],
                      "description": "For choices, \"none\" means no default."
                    },
                    "min": {
                      "type": "number"
                    },
                    "max": {
                      "type": "number"
                    },
                    "formula": {
                      "type": "string",
                      "description": "Computes the value, spreadsheet-style, e.g. \"quantity * unitPrice\" or \"SUM(lines.amount)\"."
                    },
                    "examples": {
                      "type": "array",
                      "description": "Inputs and the expected result of the formula; each becomes a test.",
                      "items": {
                        "type": "object",
                        "required": [
                          "input",
                          "expected"
                        ],
                        "additionalProperties": false,
                        "properties": {
                          "input": {
                            "type": "object"
                          },
                          "expected": {}
                        }
                      }
                    },
                    "description": {
                      "type": "string",
                      "description": "What this is. Meaning for AI and developers; not shown to visitors."
                    },
                    "intent": {
                      "type": "string",
                      "description": "The outcome or experience this should achieve."
                    },
                    "rules": {
                      "type": "array",
                      "description": "Domain rules in plain language. A rule does nothing by itself until something implements it.",
                      "items": {
                        "oneOf": [
                          {
                            "type": "string"
                          },
                          {
                            "type": "object",
                            "required": [
                              "rule"
                            ],
                            "additionalProperties": false,
                            "properties": {
                              "id": {
                                "type": "string"
                              },
                              "rule": {
                                "type": "string"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                }
              ]
            }
          },
          "ownedBy": {
            "type": "string",
            "description": "A User field whose user owns the record, instead of its creator."
          },
          "access": {
            "type": "object",
            "description": "Exceptions to the default access in smithable.auth.yml, e.g. { delete: [admin] }. \"owner\" is the record's owner.",
            "additionalProperties": false,
            "properties": {
              "read": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "create": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "write": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "delete": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "capacity": {
            "type": "object",
            "description": "At most max records per record that per refers to, e.g. { per: session, max: session.capacity }: a booking's places.",
            "required": [
              "per",
              "max"
            ],
            "additionalProperties": false,
            "properties": {
              "per": {
                "type": "string",
                "description": "A required reference of this model."
              },
              "max": {
                "oneOf": [
                  {
                    "type": "integer",
                    "minimum": 1
                  },
                  {
                    "type": "string"
                  }
                ],
                "description": "A whole number, or a whole-number field of the referenced record, \"reference.field\"."
              },
              "where": {
                "type": "object",
                "description": "Only records with these choice or yes/no values count."
              }
            }
          },
          "description": {
            "type": "string",
            "description": "What this is. Meaning for AI and developers; not shown to visitors."
          },
          "intent": {
            "type": "string",
            "description": "The outcome or experience this should achieve."
          },
          "rules": {
            "type": "array",
            "description": "Domain rules in plain language. A rule does nothing by itself until something implements it.",
            "items": {
              "oneOf": [
                {
                  "type": "string"
                },
                {
                  "type": "object",
                  "required": [
                    "rule"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "rule": {
                      "type": "string"
                    }
                  }
                }
              ]
            }
          }
        }
      }
    },
    "actions": {
      "type": "object",
      "description": "Operations of the application beyond create, edit and delete.",
      "propertyNames": {
        "pattern": "^[a-z][A-Za-z0-9]*$"
      },
      "additionalProperties": {
        "type": "object",
        "required": [
          "custom"
        ],
        "additionalProperties": false,
        "properties": {
          "custom": {
            "const": true,
            "description": "Implemented in src/custom/actions/."
          },
          "input": {
            "type": "object",
            "propertyNames": {
              "pattern": "^[a-z][A-Za-z0-9]*$"
            },
            "additionalProperties": {
              "type": "string",
              "description": "Field shorthand, e.g. \"Customer!\" or \"int\"."
            },
            "description": "The values the action needs."
          },
          "output": {
            "type": "object",
            "propertyNames": {
              "pattern": "^[a-z][A-Za-z0-9]*$"
            },
            "additionalProperties": {
              "type": "string",
              "description": "Field shorthand, e.g. \"Customer!\" or \"int\"."
            },
            "description": "The values the action returns, if any."
          },
          "allow": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Only these roles may run the action (admins always can)."
          },
          "description": {
            "type": "string",
            "description": "What this is. Meaning for AI and developers; not shown to visitors."
          },
          "intent": {
            "type": "string",
            "description": "The outcome or experience this should achieve."
          },
          "rules": {
            "type": "array",
            "description": "Domain rules in plain language. A rule does nothing by itself until something implements it.",
            "items": {
              "oneOf": [
                {
                  "type": "string"
                },
                {
                  "type": "object",
                  "required": [
                    "rule"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "rule": {
                      "type": "string"
                    }
                  }
                }
              ]
            }
          }
        }
      }
    },
    "queries": {
      "type": "object",
      "description": "Records chosen or ordered by code in src/custom/queries/.",
      "propertyNames": {
        "pattern": "^[a-z][A-Za-z0-9]*$"
      },
      "additionalProperties": {
        "type": "object",
        "required": [
          "custom",
          "returns"
        ],
        "additionalProperties": false,
        "properties": {
          "custom": {
            "const": true,
            "description": "Implemented in src/custom/queries/."
          },
          "returns": {
            "type": "string",
            "pattern": "^[A-Z][A-Za-z0-9]*\\[\\]$",
            "description": "The records it returns, e.g. \"Customer[]\"."
          },
          "description": {
            "type": "string",
            "description": "What this is. Meaning for AI and developers; not shown to visitors."
          },
          "intent": {
            "type": "string",
            "description": "The outcome or experience this should achieve."
          },
          "rules": {
            "type": "array",
            "description": "Domain rules in plain language. A rule does nothing by itself until something implements it.",
            "items": {
              "oneOf": [
                {
                  "type": "string"
                },
                {
                  "type": "object",
                  "required": [
                    "rule"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "rule": {
                      "type": "string"
                    }
                  }
                }
              ]
            }
          }
        }
      }
    },
    "pages": {
      "type": "object",
      "description": "The application's pages. Each page uses one pattern: crud, landing, dashboard, settings, onboarding, users, data, calendar, calculator, articles or custom.",
      "propertyNames": {
        "pattern": "^[a-z][A-Za-z0-9]*(?:-[a-z0-9]+)*$"
      },
      "additionalProperties": {
        "type": "object",
        "oneOf": [
          {
            "required": [
              "crud"
            ]
          },
          {
            "required": [
              "landing"
            ]
          },
          {
            "required": [
              "dashboard"
            ]
          },
          {
            "required": [
              "settings"
            ]
          },
          {
            "required": [
              "onboarding"
            ]
          },
          {
            "required": [
              "users"
            ]
          },
          {
            "required": [
              "data"
            ]
          },
          {
            "required": [
              "calendar"
            ]
          },
          {
            "required": [
              "calculator"
            ]
          },
          {
            "required": [
              "articles"
            ]
          },
          {
            "required": [
              "custom"
            ]
          }
        ],
        "additionalProperties": false,
        "properties": {
          "route": {
            "type": "string",
            "pattern": "^\\/(?:[a-z0-9]+(?:-[a-z0-9]+)*(?:\\/[a-z0-9]+(?:-[a-z0-9]+)*)*)?$",
            "description": "Defaults to / plus the page name."
          },
          "crud": {
            "type": "string",
            "description": "A model to list, view, create, edit and delete."
          },
          "landing": {
            "type": "object",
            "description": "Sections of a landing page, in order: hero, features, records, pricing, testimonials, faq, callToAction, contact, gallery, footer, calculator, custom.",
            "additionalProperties": {
              "type": [
                "object",
                "null"
              ],
              "additionalProperties": false,
              "properties": {
                "records": {
                  "type": "string",
                  "description": "A model whose records the section shows."
                },
                "search": {
                  "type": "boolean",
                  "description": "A records section with a search box, leading to the model's list page."
                },
                "filters": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  },
                  "description": "A records section's filters (fields of the model), leading to its list page."
                },
                "button": {
                  "type": "string",
                  "description": "The page the section's button opens."
                },
                "shortcuts": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  },
                  "maxItems": 4,
                  "description": "A hero's shortcuts: up to four pages, shown as round icons."
                },
                "limit": {
                  "type": "integer",
                  "minimum": 1
                },
                "form": {
                  "type": "string",
                  "description": "A contact section's form: the model visitors add a record of."
                },
                "calculator": {
                  "type": "object",
                  "required": [
                    "fields"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "fields": {
                      "type": "object",
                      "description": "Fields as a model's: entered ones are the inputs, formula fields the results.",
                      "additionalProperties": true
                    },
                    "examples": {
                      "type": "array",
                      "description": "Inputs and the results they must give; each becomes a test.",
                      "items": {
                        "type": "object",
                        "required": [
                          "input",
                          "expected"
                        ],
                        "properties": {
                          "input": {
                            "type": "object"
                          },
                          "expected": {
                            "type": "object"
                          }
                        }
                      }
                    }
                  },
                  "description": "Inputs and results computed as you type, stored nowhere."
                },
                "custom": {
                  "type": "string",
                  "description": "A section of your own: the component in src/custom/sections/<Name>.tsx."
                }
              }
            }
          },
          "dashboard": {
            "type": "object",
            "description": "Tiles of a dashboard, in order: numbers, charts by month, latest records.",
            "additionalProperties": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "count": {
                  "type": "string",
                  "description": "Count a model's records, e.g. Invoice."
                },
                "sum": {
                  "type": "string",
                  "description": "Sum a number field, e.g. Invoice.total."
                },
                "average": {
                  "type": "string",
                  "description": "Average a number field."
                },
                "list": {
                  "type": "string",
                  "description": "Show a model's latest records."
                },
                "query": {
                  "type": "string",
                  "description": "Show a custom query's records."
                },
                "where": {
                  "type": "object",
                  "description": "Only records with these choice values or yes/no values."
                },
                "by": {
                  "type": "string",
                  "description": "A date field: one bar per month."
                },
                "limit": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            }
          },
          "settings": {
            "type": "string",
            "description": "A model with one record per user (private data) or per app (shared): the page edits it."
          },
          "onboarding": {
            "type": "object",
            "required": [
              "model",
              "steps"
            ],
            "additionalProperties": false,
            "description": "Fills the settings record in steps after the first sign-in.",
            "properties": {
              "model": {
                "type": "string"
              },
              "steps": {
                "type": "object",
                "additionalProperties": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              }
            }
          },
          "users": {
            "type": "string",
            "enum": [
              "manage"
            ],
            "description": "Who has access: invite people by email with a role, change roles and remove users. For admins, unless the page's access says otherwise."
          },
          "data": {
            "oneOf": [
              {
                "type": "string",
                "enum": [
                  "all"
                ]
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "minItems": 1
              }
            ],
            "description": "An administrator's view of the app's data: all (every model) or a list of models, each with its records to find, open, change and delete. For admins, unless the page's access says otherwise."
          },
          "calendar": {
            "oneOf": [
              {
                "type": "string",
                "description": "A model whose records have one date or date-time field."
              },
              {
                "type": "object",
                "required": [
                  "model"
                ],
                "additionalProperties": false,
                "properties": {
                  "model": {
                    "type": "string"
                  },
                  "starts": {
                    "type": "string",
                    "description": "The date or date-time field that places a record."
                  },
                  "ends": {
                    "type": "string",
                    "description": "An end field of the same kind."
                  },
                  "duration": {
                    "type": "string",
                    "description": "Minutes: a whole-number field, here or one reference away (classType.durationMinutes)."
                  },
                  "title": {
                    "type": "string",
                    "description": "What a block says: a field, or a reference's title."
                  }
                }
              }
            ],
            "description": "A model's records in a week or month calendar."
          },
          "calculator": {
            "type": "object",
            "required": [
              "fields"
            ],
            "additionalProperties": false,
            "properties": {
              "fields": {
                "type": "object",
                "description": "Fields as a model's: entered ones are the inputs, formula fields the results.",
                "additionalProperties": true
              },
              "examples": {
                "type": "array",
                "description": "Inputs and the results they must give; each becomes a test.",
                "items": {
                  "type": "object",
                  "required": [
                    "input",
                    "expected"
                  ],
                  "properties": {
                    "input": {
                      "type": "object"
                    },
                    "expected": {
                      "type": "object"
                    }
                  }
                }
              }
            },
            "description": "Inputs and results computed as you type, stored nowhere."
          },
          "articles": {
            "oneOf": [
              {
                "type": "string",
                "description": "A model with a title and a markdown body; its other parts are inferred."
              },
              {
                "type": "object",
                "required": [
                  "model"
                ],
                "additionalProperties": false,
                "properties": {
                  "model": {
                    "type": "string"
                  },
                  "title": {
                    "type": "string",
                    "description": "The article's title."
                  },
                  "body": {
                    "type": "string",
                    "description": "A markdown field: the article itself."
                  },
                  "picture": {
                    "type": "string",
                    "description": "A picture field shown with the article."
                  },
                  "date": {
                    "type": "string",
                    "description": "A date or date-time field: when it appeared."
                  },
                  "address": {
                    "type": "string",
                    "description": "A unique string field: the article's address (/blog/<address>)."
                  },
                  "summary": {
                    "type": "string",
                    "description": "A short text shown in the list."
                  }
                }
              }
            ],
            "description": "A model's records as articles: a blog, news or a knowledge base."
          },
          "custom": {
            "type": "string",
            "description": "A page of your own: the component in src/custom/pages/<Name>.tsx, created once as a stub; the route, access and menu entry stay Smithable's."
          },
          "actions": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Actions offered on the page, as buttons."
          },
          "access": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Only these roles may open the page (admins always can)."
          },
          "search": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Fields the list can be searched by, e.g. [name, email]."
          },
          "filters": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Fields the list can be filtered by: choices and references as chips, numbers and dates as a range, yes/no as a toggle."
          },
          "sortable": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Fields the list can be sorted by, either way, from a Sort by control."
          },
          "mine": {
            "type": "boolean",
            "description": "The page lists only the viewer's own records (my items, my orders); it needs sign-in."
          },
          "favourites": {
            "oneOf": [
              {
                "type": "boolean"
              },
              {
                "const": "only"
              }
            ],
            "description": "true: a heart on each record and a Saved view of the list; only: a list of the viewer's saved records."
          },
          "contact": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "object",
                "required": [
                  "to"
                ],
                "additionalProperties": false,
                "properties": {
                  "to": {
                    "type": "string",
                    "description": "owner, or a user field of the model."
                  },
                  "keep": {
                    "type": "string",
                    "description": "A model a copy of each message is stored in."
                  }
                }
              }
            ],
            "description": "A contact block on the record page that mails the record's owner (or the user a field names): owner, a field, or { to, keep }."
          },
          "description": {
            "type": "string",
            "description": "What this is. Meaning for AI and developers; not shown to visitors."
          },
          "intent": {
            "type": "string",
            "description": "The outcome or experience this should achieve."
          },
          "rules": {
            "type": "array",
            "description": "Domain rules in plain language. A rule does nothing by itself until something implements it.",
            "items": {
              "oneOf": [
                {
                  "type": "string"
                },
                {
                  "type": "object",
                  "required": [
                    "rule"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "rule": {
                      "type": "string"
                    }
                  }
                }
              ]
            }
          }
        }
      }
    },
    "navigation": {
      "description": "Menus: a list of pages (the main menu), or named menus. Defaults to all pages but landing and onboarding pages, in order.",
      "oneOf": [
        {
          "type": "array",
          "items": {
            "type": "string"
          },
          "uniqueItems": true
        },
        {
          "type": "object",
          "propertyNames": {
            "pattern": "^[a-z][A-Za-z0-9]*(?:-[a-z0-9]+)*$"
          },
          "additionalProperties": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "uniqueItems": true
          }
        }
      ]
    }
  }
}
