diff --git a/internal/typegen/common.go b/internal/typegen/common.go new file mode 100644 index 0000000..4f8aa10 --- /dev/null +++ b/internal/typegen/common.go @@ -0,0 +1,69 @@ +package typegen + +import ( + "io" +) + +type TypeGenerator struct { + writer io.Writer + GeneratePermissionTypes func() error + GenerateRoleTypes func() error + GenerateAuditLogTypes func() error +} + +func NewTypeGenerator(w io.Writer) *TypeGenerator { + return &TypeGenerator{writer: w} +} + +// Permission types +type PermissionResponse struct { + Data []Permission `json:"data"` +} + +type Permission struct { + Slug string `json:"slug"` +} + +// Role types +type RoleResponse struct { + Data []Role `json:"data"` +} + +type Role struct { + Slug string `json:"slug"` +} + +// Audit Log types +type AuditLogResponse struct { + Data []AuditLogEvent `json:"data"` +} + +type AuditLogEvent struct { + Name string `json:"name"` + Schema AuditLogSchema `json:"schema"` +} + +type AuditLogSchema struct { + ActorSchema AuditLogActorSchema `json:"actor"` + TargetSchemas []AuditLogTargetSchema `json:"targets"` + MetadataSchema *AuditLogMetadataSchema `json:"metadata"` +} + +type AuditLogActorSchema struct { + MetadataSchema *AuditLogMetadataSchema `json:"metadata"` +} + +type AuditLogTargetSchema struct { + Type string `json:"type"` + MetadataSchema *AuditLogMetadataSchema `json:"metadata"` +} + +type AuditLogMetadataSchema struct { + Type string `json:"type"` + Properties map[string]AuditLogMetadataProperty `json:"properties"` +} + +type AuditLogMetadataProperty struct { + Type string `json:"type"` + Nullable *bool `json:"nullable,omitempty"` +} diff --git a/internal/typegen/python_generator.go b/internal/typegen/python_generator.go new file mode 100644 index 0000000..71d13c7 --- /dev/null +++ b/internal/typegen/python_generator.go @@ -0,0 +1,250 @@ +package typegen + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" + + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +type PythonTypeGenerator struct { + TypeGenerator +} + +func NewPythonTypeGenerator(w io.Writer) *PythonTypeGenerator { + return &PythonTypeGenerator{TypeGenerator: TypeGenerator{writer: w}} +} + +// Permission Generator +func (g *PythonTypeGenerator) GeneratePermissionsTypes() error { + jsonData, err := os.ReadFile("permissions.json") + if err != nil { + return err + } + + var response PermissionResponse + if err := json.Unmarshal(jsonData, &response); err != nil { + return fmt.Errorf("failed to parse permissions JSON: %w", err) + } + + slugs := make([]string, len(response.Data)) + for i, permission := range response.Data { + slugs[i] = permission.Slug + } + + return g.generateUnionType("WorkOSPermission", slugs) +} + +// Role Generator +func (g *PythonTypeGenerator) GenerateRolesTypes() error { + jsonData, err := os.ReadFile("roles.json") + if err != nil { + return err + } + + var response RoleResponse + if err := json.Unmarshal(jsonData, &response); err != nil { + return fmt.Errorf("failed to parse roles JSON: %w", err) + } + + slugs := make([]string, len(response.Data)) + for i, role := range response.Data { + slugs[i] = role.Slug + } + + return g.generateUnionType("WorkOSRole", slugs) +} + +// Audit Log Generator +func (g *PythonTypeGenerator) GenerateAuditLogsTypes() error { + req, err := http.NewRequest("GET", "https://api.workos.com/audit_logs/actions", nil) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + req.Header.Set("Authorization", "Bearer "+os.Getenv("WORKOS_API_KEY")) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("failed to fetch audit logs actions: %w", err) + } + defer resp.Body.Close() + + var response AuditLogResponse + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return fmt.Errorf("failed to parse audit logs response: %w", err) + } + + var interfaces []string + var eventNames []string + + for _, action := range response.Data { + interfaceName := g.generateInterfaceName(action.Name) + eventNames = append(eventNames, interfaceName) + + interfaceContent := g.generateAuditLogEventInterface(action) + interfaces = append(interfaces, interfaceContent) + } + + // Add typing imports + output := "from typing import Literal, NotRequired, Sequence, TypedDict, Union\n\n" + + output += strings.Join(interfaces, "\n") + + unionType := fmt.Sprintf("\nAuditLogEvent = Union[%s]\n", + strings.Join(eventNames, ", ")) + output += unionType + + _, err = fmt.Fprint(g.writer, output) + return err +} + +func (g *PythonTypeGenerator) generateTypedDictFromMap(name string, properties map[string]string) string { + generatedCode := fmt.Sprintf("class %s(TypedDict):\n", name) + propertiesBuffer := new(bytes.Buffer) + + for key, pythonType := range properties { + fmt.Fprintf(propertiesBuffer, " %s: %s\n", key, pythonType) + } + + generatedCode += propertiesBuffer.String() + "\n" + return generatedCode +} + +func (g *PythonTypeGenerator) generateTypedDictFromMetadataProperties(className string, metadataPropertyMap map[string]AuditLogMetadataProperty) string { + return g.generateTypedDictFromMap(className, g.generateMetadataProperties(metadataPropertyMap)) +} + +func (g *PythonTypeGenerator) generateAuditLogEventInterface(event AuditLogEvent) string { + interfaceName := g.generateInterfaceName(event.Name) + + var generatedCode string + + // Map of top-level property keys and types + properties := map[string]string{ + "action": fmt.Sprintf("Literal['%s']", event.Name), + "occurred_at": "str", + "version": "NotRequired[int]", + } + + actorProperties := map[string]string{ + "id": "str", + "name": "NotRequired[str]", + } + + if event.Schema.ActorSchema.MetadataSchema != nil { + actorMetadataInterfaceName := interfaceName + "ActorMetadata" + generatedCode += g.generateTypedDictFromMetadataProperties(actorMetadataInterfaceName, event.Schema.ActorSchema.MetadataSchema.Properties) + actorProperties["metadata"] = actorMetadataInterfaceName + } + + // Add actor property + actorInterfaceName := interfaceName + "Actor" + generatedCode += g.generateTypedDictFromMap(actorInterfaceName, actorProperties) + properties["actor"] = actorInterfaceName + + if len(event.Schema.TargetSchemas) > 0 { + var targetTypes []string + for _, target := range event.Schema.TargetSchemas { + targetProps := map[string]string{ + "type": fmt.Sprintf("Literal['%s']", target.Type), + "id": "str", + "name": "NotRequired[str]", + } + if target.MetadataSchema != nil { + targetMetadataInterfaceName := interfaceName + "TargetMetadata" + generatedCode += g.generateTypedDictFromMetadataProperties(targetMetadataInterfaceName, target.MetadataSchema.Properties) + targetProps["metadata"] = targetMetadataInterfaceName + } + + targetInterfaceName := fmt.Sprintf("%s%s%s", interfaceName, cases.Title(language.English).String(target.Type), "Target") + generatedCode += g.generateTypedDictFromMap(targetInterfaceName, targetProps) + targetTypes = append(targetTypes, targetInterfaceName) + } + + if len(targetTypes) > 1 { + properties["targets"] = fmt.Sprintf("Sequence[Union[%s]]", strings.Join(targetTypes, ", ")) + } else { + properties["targets"] = fmt.Sprintf("Sequence[%s]", targetTypes[0]) + } + } + + // Add context property + contextInterfaceName := interfaceName + "Context" + generatedCode += g.generateTypedDictFromMap(contextInterfaceName, map[string]string{ + "location": "str", + "user_agent": "NotRequired[str]", + }) + properties["context"] = contextInterfaceName + + // Add top-level metadata property + if event.Schema.MetadataSchema != nil && len(event.Schema.MetadataSchema.Properties) > 0 { + metadataInterfaceName := interfaceName + "Metadata" + generatedCode += g.generateTypedDictFromMetadataProperties(metadataInterfaceName, event.Schema.MetadataSchema.Properties) + properties["metadata"] = metadataInterfaceName + } + + generatedCode += fmt.Sprintf("# Types for %ss\nclass %s(TypedDict):\n", interfaceName, interfaceName) + propertiesBuffer := new(bytes.Buffer) + + for key, pythonType := range properties { + fmt.Fprintf(propertiesBuffer, " %s: %s\n", key, pythonType) + } + + generatedCode += propertiesBuffer.String() + return generatedCode +} + +func (g *PythonTypeGenerator) generateMetadataProperties(properties map[string]AuditLogMetadataProperty) map[string]string { + convertedProperties := make(map[string]string) + for key, prop := range properties { + if prop.Nullable != nil && *prop.Nullable { + convertedProperties[key] = fmt.Sprintf("NotRequired[%s]", prop.Type) + } else { + convertedProperties[key] = g.convertMetadataType(prop.Type) + } + } + + return convertedProperties +} + +func (g *PythonTypeGenerator) convertMetadataType(propertyType string) string { + switch propertyType { + case "string": + return "str" + default: + return propertyType + } +} + +// Utils +func (g *PythonTypeGenerator) generateInterfaceName(action string) string { + parts := strings.Split(action, ".") + var name string + for _, part := range parts { + part = strings.ReplaceAll(part, "_", " ") + words := strings.Fields(part) + for _, word := range words { + name += cases.Title(language.English).String(word) + } + } + + return name + "AuditLogEvent" +} + +func (g *PythonTypeGenerator) generateUnionType(typeName string, values []string) error { + quotedValues := make([]string, len(values)) + for i, v := range values { + quotedValues[i] = fmt.Sprintf("'%s'", v) + } + + output := fmt.Sprintf("%s = Literal[%s]\n", typeName, strings.Join(quotedValues, ", ")) + _, err := fmt.Fprint(g.writer, output) + + return err +} diff --git a/internal/typegen/type_generator.go b/internal/typegen/type_generator.go new file mode 100644 index 0000000..2b46295 --- /dev/null +++ b/internal/typegen/type_generator.go @@ -0,0 +1,227 @@ +package typegen + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" + + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +type TypeScriptTypeGenerator struct { + writer io.Writer + TypeGenerator +} + +func NewTypeScriptTypeGenerator(w io.Writer) *TypeScriptTypeGenerator { + return &TypeScriptTypeGenerator{writer: w, TypeGenerator: TypeGenerator{writer: w}} +} + +// Permission Generator +func (g *TypeScriptTypeGenerator) GeneratePermissionsTypes() error { + jsonData, err := os.ReadFile("permissions.json") + if err != nil { + return err + } + + var response PermissionResponse + if err := json.Unmarshal(jsonData, &response); err != nil { + return fmt.Errorf("failed to parse permissions JSON: %w", err) + } + + slugs := make([]string, len(response.Data)) + for i, permission := range response.Data { + slugs[i] = permission.Slug + } + + return g.generateUnionType("WorkOSPermission", slugs) +} + +// Role Generator +func (g *TypeGenerator) GenerateRolesTypes() error { + jsonData, err := os.ReadFile("roles.json") + if err != nil { + return err + } + + var response RoleResponse + if err := json.Unmarshal(jsonData, &response); err != nil { + return fmt.Errorf("failed to parse roles JSON: %w", err) + } + + slugs := make([]string, len(response.Data)) + for i, role := range response.Data { + slugs[i] = role.Slug + } + + return g.generateUnionType("WorkOSRole", slugs) +} + +// Audit Log Generator +func (g *TypeScriptTypeGenerator) GenerateAuditLogsTypes() error { + req, err := http.NewRequest("GET", "https://api.workos.com/audit_logs/actions", nil) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + req.Header.Set("Authorization", "Bearer "+os.Getenv("WORKOS_API_KEY")) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("failed to fetch audit logs actions: %w", err) + } + defer resp.Body.Close() + + var response AuditLogResponse + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return fmt.Errorf("failed to parse audit logs response: %w", err) + } + + var interfaces []string + var eventNames []string + + for _, action := range response.Data { + interfaceName := g.generateInterfaceName(action.Name) + "AuditLogEvent" + eventNames = append(eventNames, interfaceName) + + interfaceContent := g.generateAuditLogEventInterface(action) + interfaces = append(interfaces, interfaceContent) + } + + output := strings.Join(interfaces, "\n\n") + + unionType := fmt.Sprintf("\nexport type AuditLogEvent = %s;\n", + strings.Join(eventNames, " | ")) + output += unionType + + _, err = fmt.Fprint(g.writer, output) + return err +} + +func (g *TypeScriptTypeGenerator) generateAuditLogEventInterface(event AuditLogEvent) string { + interfaceName := g.generateInterfaceName(event.Name) + + var properties []string + + properties = append(properties, fmt.Sprintf(" action: '%s';", event.Name)) + properties = append(properties, " occurredAt: string;") + properties = append(properties, fmt.Sprintf(" version?: number;")) + + actorProps := []string{ + " id: string;", + " name?: string;", + } + + if event.Schema.ActorSchema.MetadataSchema != nil { + metadataProps := g.generateMetadataProperties(event.Schema.ActorSchema.MetadataSchema.Properties) + actorProps = append(actorProps, fmt.Sprintf(" metadata: {\n%s\n };", metadataProps)) + } + + properties = append(properties, fmt.Sprintf(" actor: {\n%s\n };", strings.Join(actorProps, "\n"))) + + if len(event.Schema.TargetSchemas) > 0 { + var targetTypes []string + for _, target := range event.Schema.TargetSchemas { + targetProps := []string{ + fmt.Sprintf(" type: '%s';", target.Type), + " id: string;", + " name?: string;", + } + if target.MetadataSchema != nil { + metadataProps := g.generateMetadataProperties(target.MetadataSchema.Properties) + targetProps = append(targetProps, fmt.Sprintf(" metadata: {\n%s\n };", metadataProps)) + } + + targetTypes = append(targetTypes, fmt.Sprintf("{\n%s\n}", strings.Join(targetProps, "\n"))) + } + + properties = append(properties, fmt.Sprintf(" targets: (%s)[];", strings.Join(targetTypes, " | "))) + } + + contextProps := []string{ + " location: string;", + " userAgent?: string;", + } + + properties = append(properties, fmt.Sprintf(" context: {\n%s\n };", strings.Join(contextProps, "\n"))) + + if event.Schema.MetadataSchema != nil { + metadataProps := g.generateMetadataProperties(event.Schema.MetadataSchema.Properties) + properties = append(properties, fmt.Sprintf(" metadata: {\n%s\n };", metadataProps)) + } + + return fmt.Sprintf("export interface %s {\n%s\n}", + interfaceName, + strings.Join(properties, "\n")) +} + +func (g *TypeScriptTypeGenerator) generateMetadataProperties(properties map[string]AuditLogMetadataProperty) string { + var props []string + for key, prop := range properties { + nullable := "" + if prop.Nullable != nil && *prop.Nullable { + nullable = " | null" + } + props = append(props, fmt.Sprintf(" %s: %s%s;", key, prop.Type, nullable)) + } + + return strings.Join(props, "\n") +} + +// Utils +func (g *TypeScriptTypeGenerator) generateInterfaceName(action string) string { + parts := strings.Split(action, ".") + var name string + for _, part := range parts { + part = strings.ReplaceAll(part, "_", " ") + words := strings.Fields(part) + for _, word := range words { + name += cases.Title(language.English).String(word) + } + } + + return name +} + +func (g *TypeScriptTypeGenerator) generateUnionType(typeName string, values []string) error { + quotedValues := make([]string, len(values)) + for i, v := range values { + quotedValues[i] = fmt.Sprintf("'%s'", v) + } + + output := fmt.Sprintf("export type %s = %s;\n", typeName, strings.Join(quotedValues, " | ")) + _, err := fmt.Fprint(g.writer, output) + + return err +} + +func main() { + if len(os.Args) != 2 { + fmt.Println("Usage: typegenerator ") + os.Exit(1) + } + + generator := NewTypeGenerator(os.Stdout) + var err error + + switch os.Args[1] { + case "permissions": + err = generator.GenerateFromPermissionsAPI() + case "roles": + err = generator.GenerateFromRolesAPI() + case "audit-logs": + err = generator.GenerateFromAuditLogsAPI() + default: + fmt.Printf("Unknown type: %s\n", os.Args[1]) + fmt.Println("Valid types are: permissions, roles, audit-logs") + os.Exit(1) + } + + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } +}