Skip to content

Commit

Permalink
Merge pull request #16 from workos/md5/typegen-for-python
Browse files Browse the repository at this point in the history
Add typegen for Python
  • Loading branch information
mattgd authored Dec 19, 2024
2 parents 6a9e2ad + 667a761 commit 3e2684e
Show file tree
Hide file tree
Showing 6 changed files with 492 additions and 291 deletions.
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/workos/workos-cli

go 1.23
go 1.23.4

require (
github.com/charmbracelet/huh v0.5.1
Expand All @@ -9,6 +9,7 @@ require (
github.com/spf13/cobra v1.8.1
github.com/spf13/viper v1.19.0
github.com/workos/workos-go/v4 v4.21.0
golang.org/x/text v0.16.0
)

require (
Expand Down Expand Up @@ -56,7 +57,8 @@ require (
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

replace github.com/workos/workos-go/v4 => github.com/workos/workos-go/v4 v4.26.1-0.20241219170524-3ecd219ec436
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/workos/workos-go/v4 v4.21.0 h1:pEoAJzCsBPU46dL6/PwwwS5BrBV8LWOZQp0mERrRPCc=
github.com/workos/workos-go/v4 v4.21.0/go.mod h1:CwpXdAWhIE3SxV49qBVeYqWV8ojv0A0L9nM1xnho4/c=
github.com/workos/workos-go/v4 v4.26.1-0.20241219170524-3ecd219ec436 h1:3qRQq9res1qTa68OKTwZ+xdGKKr6esRorTxWkdEM8kQ=
github.com/workos/workos-go/v4 v4.26.1-0.20241219170524-3ecd219ec436/go.mod h1:CwpXdAWhIE3SxV49qBVeYqWV8ojv0A0L9nM1xnho4/c=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
Expand Down
339 changes: 52 additions & 287 deletions internal/cmd/typegen.go
Original file line number Diff line number Diff line change
@@ -1,307 +1,72 @@
package cmd

import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"slices"

"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"github.com/workos/workos-cli/internal/typegen"
)

type TypeGenerator struct {
writer io.Writer
}

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"`
}

// Permission Generator
func (g *TypeGenerator) GenerateFromPermissionsAPI() 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 types
type RoleResponse struct {
Data []Role `json:"data"`
}

type Role struct {
Slug string `json:"slug"`
}

// Role Generator
func (g *TypeGenerator) GenerateFromRolesAPI() 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 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"`
}

// Audit Log Generator
func (g *TypeGenerator) GenerateFromAuditLogsAPI() 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 *TypeGenerator) 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))
}
const (
FlagLanguage = "language"
FlagResources = "resources"
)

targetTypes = append(targetTypes, fmt.Sprintf("{\n%s\n}", strings.Join(targetProps, "\n")))
// CLI
var typegenCmd = &cobra.Command{
Use: "generate-types",
Short: "Generate types for WorkOS resources",
Long: "A tool to generate type definitions for your WorkOS resources.",
Example: "workos generate-types --language typescript --resources permissions",
RunE: func(cmd *cobra.Command, args []string) error {
language, err := cmd.Flags().GetString(FlagLanguage)
if err != nil {
return errors.New("Invalid language flag")
}

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 *TypeGenerator) generateMetadataProperties(properties map[string]AuditLogMetadataProperty) string {
var props []string
for key, prop := range properties {
nullable := ""
if prop.Nullable != nil && *prop.Nullable {
nullable = " | null"
resources, err := cmd.Flags().GetStringArray(FlagResources)
if err != nil {
return errors.New("Invalid resources flag")
}
props = append(props, fmt.Sprintf(" %s: %s%s;", key, prop.Type, nullable))
}

return strings.Join(props, "\n")
}

// Utils
func (g *TypeGenerator) 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)
var languageGenerator typegen.LanguageTypeGenerator
switch language {
case "typescript":
languageGenerator = typegen.NewTypeScriptTypeGenerator(cmd.OutOrStdout())
case "python":
languageGenerator = typegen.NewPythonTypeGenerator(cmd.OutOrStdout())
default:
return errors.New(fmt.Sprintf("Invalid language: %s. Valid options are: typescript, python"))
}
}

return name
}

func (g *TypeGenerator) 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
}

// CLI
var typegenCmd = &cobra.Command{
Use: "typegen",
Short: "Generate TypeScript types from WorkOS APIs",
Long: `A tool to generate TypeScript type definitions from WorkOS permissions, roles, and audit logs.`,
}

func NewTypeGeneratorCmd() *cobra.Command {
cmd := typegenCmd

cmd.AddCommand(newPermissionsCmd())
cmd.AddCommand(newRolesCmd())
cmd.AddCommand(newAuditLogsCmd())

return cmd
}

func newPermissionsCmd() *cobra.Command {
return &cobra.Command{
Use: "permissions",
Short: "Generate TypeScript types for WorkOS permissions",
RunE: func(cmd *cobra.Command, args []string) error {
generator := NewTypeGenerator(cmd.OutOrStdout())
return generator.GenerateFromPermissionsAPI()
},
}
}
if err := typegen.GenerateImports(languageGenerator, resources); err != nil {
return errors.Wrap(err, "error generating imports")
}

func newRolesCmd() *cobra.Command {
return &cobra.Command{
Use: "roles",
Short: "Generate TypeScript types for WorkOS roles",
RunE: func(cmd *cobra.Command, args []string) error {
generator := NewTypeGenerator(cmd.OutOrStdout())
return generator.GenerateFromRolesAPI()
},
}
}
if slices.Contains(resources, "all") {
typegen.GeneratePermissionsType(languageGenerator)
typegen.GenerateRolesType(languageGenerator)
typegen.GenerateAuditLogsTypes(languageGenerator)
} else {
for _, resource := range resources {
switch resource {
case "permissions":
typegen.GeneratePermissionsType(languageGenerator)
case "roles":
typegen.GenerateRolesType(languageGenerator)
case "audit-logs":
typegen.GenerateAuditLogsTypes(languageGenerator)
}
}
}

func newAuditLogsCmd() *cobra.Command {
return &cobra.Command{
Use: "audit-logs",
Short: "Generate TypeScript types for WorkOS audit logs",
RunE: func(cmd *cobra.Command, args []string) error {
generator := NewTypeGenerator(cmd.OutOrStdout())
return generator.GenerateFromAuditLogsAPI()
},
}
return nil
},
}

func init() {
cmd := NewTypeGeneratorCmd()
rootCmd.AddCommand(cmd)
typegenCmd.Flags().StringP(FlagLanguage, "l", "typescript", "Language to to output types in")
typegenCmd.Flags().StringArrayP(FlagResources, "r", []string{"all"}, "Resources to output types for")
rootCmd.AddCommand(typegenCmd)
}
Loading

0 comments on commit 3e2684e

Please sign in to comment.