Files
clarizen-lib/scripts/generate-api-types.py
2026-06-04 12:09:45 -03:00

253 lines
8.5 KiB
Python

#!/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<string, JsonValue>",
"RoleType": "string",
}
PRIMITIVE_MAP = {
"string": "string",
"boolean": "boolean",
"integer": "number",
"long": "number",
"double": "number",
"datetime": "string",
"date": "string",
"type": "Record<string, JsonValue>",
"json object": "Record<string, JsonValue>",
"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()