#!/usr/bin/env python3 """Generate src/clarizen-api-types.ts from clarizen-schema.json.""" import json import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] SCHEMA = ROOT / "src" / "generated" / "clarizen-schema.json" OUT = ROOT / "src" / "clarizen-api-types.ts" ENUMS = { "Operator": [ "Equal", "NotEqual", "LessThan", "GreaterThan", "LessThanOrEqual", "GreaterThanOrEqual", "BeginsWith", "EndsWith", "Contains", "DoesNotContain", "In", "Like", "InRange", "NotInRange", "NotIn", ], "Order": ["Ascending", "Descending"], "DatePeriod": [ "ThisWeek", "ThisMonth", "ThisQuarter", "ThisYear", "NextWeek", "NextMonth", "NextQuarter", "NextYear", "LastWeek", "LastMonth", "LastQuarter", "LastYear", ], "FunctionType": ["Count", "Max", "Min", "Sum", "Avg"], "LicenseType": [ "Full", "Limited", "Email", "None", "TeamMember", "Social", "ExternalCollaborator", ], "NewsFeedMode": ["Following", "All"], "TimesheetState": ["UnSubmitted", "PendingApproval", "Approved", "All"], "ErrorCode": [ "EntityNotFound", "InvalidArgument", "MissingArgument", "InvalidOperation", "DuplicateKey", "InvalidField", "InvalidType", "FileNotFound", "VirusCheckFailed", "Unauthorized", "UnsupportedClient", "General", "Internal", "ValidationRuleError", "LoginFailure", "SessionTimeout", "Redirect", "InvalidQuery", "ExecutionThresholdExceeded", "TooManyRequests", ], "FieldType": [ "Boolean", "String", "Integer", "Long", "Double", "DateTime", "Date", "Entity", "Duration", "Money", ], "PresentationType": [ "Text", "Numeric", "Date", "Time", "Checkbox", "TextArea", "Currency", "Duration", "ReferenceToObject", "PickList", "Url", "Percent", "Other", ], "RecipientType": ["To", "CC"], "AccessType": ["Public", "Private"], "TriggerType": [ "Create", "CreateOrEdit", "Delete", "CreateOrEditWithPreviousValueNotEqual", ], "HttpMethod": ["DELETE", "GET", "POST", "PUT"], } STUB_TYPES = { "ApplyOnMembersResult": "string", "IFormat": "Record", "RoleType": "string", } PRIMITIVE_MAP = { "string": "string", "boolean": "boolean", "integer": "number", "long": "number", "double": "number", "datetime": "string", "date": "string", "type": "Record", "json object": "Record", "object": "JsonValue", "entityid": "EntityId", } def pascal(name: str) -> str: if not name: return name if name[0].isupper(): return name.replace(" ", "") return name[0].upper() + name[1:] ARRAY_PROP_NAMES = frozenset({ "fields", "groupby", "orders", "relations", "aggregations", "feeditemoptions", "workitems", "validstates", }) def ts_prop_type(prop: dict) -> str: pname = prop.get("name", "").rstrip("*").lower() href = prop.get("typeHref") or "" if href: tail = href.rstrip("/").split("/")[-1] tail = pascal(tail) if tail in ("ListOf_String", "Array_String"): return "string[]" if pname in ARRAY_PROP_NAMES or tail == "FieldAggregation": if tail == "FieldAggregation": return "FieldAggregation[]" if tail == "FieldDescription": return "FieldDescription[]" if tail == "RelationDescription": return "RelationDescription[]" if tail == "OrderBy": return "OrderBy[]" if tail == "Relation": return "Relation[]" return f"{tail}[]" return tail raw = prop.get("type", "JsonValue").lower() raw = re.sub(r"\(.*\)", "", raw).strip() if raw.endswith("[]"): inner = PRIMITIVE_MAP.get(raw[:-2], pascal(raw[:-2])) return f"{inner}[]" return PRIMITIVE_MAP.get(raw, "JsonValue") def emit_enum(name: str, values: list[str]) -> str: lines = [f"export type {name} ="] for v in values: lines.append(f' | "{v}"') lines[-1] = lines[-1] + ";" return "\n".join(lines) def prop_optional(prop_name: str) -> str: return "" if prop_name.endswith("*") else "?" def clean_prop_name(prop_name: str) -> str: return prop_name.rstrip("*") def emit_interface(name: str, variant: dict) -> str: props = variant.get("properties", []) if not props and "(enum)" in variant.get("variant", ""): return "" lines = [f"export interface {name} {{"] for p in props: pname = clean_prop_name(p["name"]) lines.append(f" {pname}{prop_optional(p['name'])}: {ts_prop_type(p)};") lines.append("}") return "\n".join(lines) def main(): data = json.loads(SCHEMA.read_text()) parsed = data["parsed"] out: list[str] = [ "// Auto-generated from Clarizen REST API V2.0 type docs. Run: npm run generate:types", 'import type { JsonValue } from "./types.js";', "", "export type EntityId = string;", "", ] for enum_name, values in ENUMS.items(): out.append(emit_enum(enum_name, values)) out.append("") # Condition union (manual structure from variants) out.append("export interface AndCondition { and: Condition[]; }") out.append("export interface OrCondition { or: Condition[]; }") out.append("export interface NotCondition { not: Condition; }") out.append("export interface CompareCondition {") out.append(" _type?: \"Compare\";") out.append(" leftExpression: Expression;") out.append(" operator: ComparisonOperator;") out.append(" rightExpression: Expression;") out.append("}") out.append("export interface CzqlCondition {") out.append(" text: string;") out.append(" parameters?: Parameters;") out.append("}") out.append( "export type Condition = AndCondition | OrCondition | NotCondition | CompareCondition | CzqlCondition;" ) out.append("") # Expression union out.append("export interface FieldExpression { fieldName: string; }") out.append("export interface ConstantExpression { value: JsonValue; }") out.append("export interface ConstantListExpression { values: JsonValue[]; }") out.append("export interface QueryExpression { query: Query; }") out.append("export interface PredefinedDateRangeExpression {") out.append(" value?: DatePeriod;") out.append(" dateRangeValue?: DatePeriod;") out.append("}") out.append( "export type Expression = FieldExpression | ConstantExpression | ConstantListExpression | QueryExpression | PredefinedDateRangeExpression;" ) out.append("") # Single-variant interfaces from schema skip = { "Condition", "Expression", "Operator", "Order", "Query", "ListOf_String", "Array_String", "EntityId", } for type_name, stub in STUB_TYPES.items(): out.append(f"export type {type_name} = {stub};") out.append("") for type_name in sorted(parsed.keys()): if type_name in skip or type_name in STUB_TYPES: continue info = parsed[type_name] if len(info["variants"]) != 1: continue v = info["variants"][0] if "(enum)" in v.get("variant", ""): continue iface = emit_interface(type_name, v) if iface: out.append(iface) out.append("") # Query variants query_info = parsed.get("Query", {}) query_names: list[str] = [] for v in query_info.get("variants", []): variant_key = v["variant"] iface_name = pascal(variant_key) if iface_name == "CZQLQuery": iface_name = "CzqlQuery" props = v.get("properties", []) lines = [f"export interface {iface_name} {{"] for p in props: pname = clean_prop_name(p["name"]) lines.append(f" {pname}{prop_optional(p['name'])}: {ts_prop_type(p)};") lines.append("}") out.append("\n".join(lines)) out.append("") query_names.append(iface_name) out.append(f"export type Query = {' | '.join(query_names)};") out.append("") # Shared query body (entityQuery variant) for EntityQuery operation out.append("export type EntityQueryBody = EntityQuery;") out.append("") out.append("export type ComparisonOperator = Operator;") out.append("export type SortOrder = Order;") out.append("export type ConditionParameters = Parameters;") text = "\n".join(out) + "\n" OUT.write_text(text) print(f"Wrote {OUT} ({len(text)} bytes)") if __name__ == "__main__": main()