253 lines
8.5 KiB
Python
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()
|