Skip to content

Commit

Permalink
Add typegen for Python.
Browse files Browse the repository at this point in the history
  • Loading branch information
mattgd committed Dec 18, 2024
1 parent 85fdcab commit c08ed3d
Show file tree
Hide file tree
Showing 3 changed files with 546 additions and 0 deletions.
69 changes: 69 additions & 0 deletions internal/typegen/common.go
Original file line number Diff line number Diff line change
@@ -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"`
}
250 changes: 250 additions & 0 deletions internal/typegen/python_generator.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit c08ed3d

Please sign in to comment.