diff --git a/cmd/codegen/generator/generator.go b/cmd/codegen/generator/generator.go index 8b8e7367834..83b581cc2ba 100644 --- a/cmd/codegen/generator/generator.go +++ b/cmd/codegen/generator/generator.go @@ -58,6 +58,10 @@ type Config struct { // name is the expected value. IsInit bool + // TypeDefsOnly indicates whether only type definitions should be generated, excluding other related code artifacts. + // This is used to generate module own's types even if the module doesn't compile. + TypeDefsOnly bool + // ClientOnly indicates that the codegen should only generate the client code. ClientOnly bool diff --git a/cmd/codegen/generator/go/templates/modules.go b/cmd/codegen/generator/go/templates/modules.go index f144179d787..8eb7ee0b1bb 100644 --- a/cmd/codegen/generator/go/templates/modules.go +++ b/cmd/codegen/generator/go/templates/modules.go @@ -284,11 +284,18 @@ func (funcs goTemplateFuncs) moduleMainSrc() (string, error) { //nolint: gocyclo tps, nextTps = nextTps, nil } - return strings.Join([]string{ - fmt.Sprintf("%#v", implementationCode), - mainSrc(funcs.CheckVersionCompatibility), - invokeSrc(objFunctionCases, createMod), - }, "\n"), nil + var out []string + if !funcs.cfg.TypeDefsOnly { + out = append(out, fmt.Sprintf("%#v", implementationCode)) + } + out = append(out, + mainSrc(funcs.CheckVersionCompatibility, funcs.cfg.TypeDefsOnly), + registerSrc(createMod), + ) + if !funcs.cfg.TypeDefsOnly { + out = append(out, invokeSrc(objFunctionCases)) + } + return strings.Join(out, "\n"), nil } func dotLine(a *Statement, id string) *Statement { @@ -296,61 +303,68 @@ func dotLine(a *Statement, id string) *Statement { } const ( - parentJSONVar = "parentJSON" - parentNameVar = "parentName" - fnNameVar = "fnName" - inputArgsVar = "inputArgs" - invokeFuncName = "invoke" + parentJSONVar = "parentJSON" + parentNameVar = "parentName" + fnNameVar = "fnName" + inputArgsVar = "inputArgs" + invokeFuncName = "invoke" + registerFuncName = "register" ) // mainSrc returns the static part of the generated code. It calls out to the // "invoke" func, which is the mostly dynamically generated code that actually // calls the user's functions. -func mainSrc(checkVersionCompatibility func(string) bool) string { +func mainSrc(checkVersionCompatibility func(string) bool, typeDefsOnly bool) string { // Ensure compatibility with modules that predate Void return value handling voidRet := `err` if !checkVersionCompatibility("v0.12.0") { voidRet = `_, err` } - return `func main() { - ctx := context.Background() - - // Direct slog to the new stderr. This is only for dev time debugging, and - // runtime errors/warnings. - slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ - Level: slog.LevelWarn, - }))) + var dispatch string + if typeDefsOnly { + dispatch = `func dispatch(ctx context.Context) (rerr error) { + ctx = telemetry.InitEmbedded(ctx, resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceNameKey.String("dagger-go-sdk"), + // TODO version? + )) + defer telemetry.Close() - if err := dispatch(ctx); err != nil { - os.Exit(2) - } -} + // A lot of the "work" actually happens when we're marshalling the return + // value, which entails getting object IDs, which happens in MarshalJSON, + // which has no ctx argument, so we use this lovely global variable. + setMarshalContext(ctx) -func convertError(rerr error) *dagger.Error { - var gqlErr *gqlerror.Error - if errors.As(rerr, &gqlErr) { - dagErr := dag.Error(gqlErr.Message) - if gqlErr.Extensions != nil { - keys := make([]string, 0, len(gqlErr.Extensions)) - for k := range gqlErr.Extensions { - keys = append(keys, k) - } - sort.Strings(keys) - for _, k := range keys { - val, err := json.Marshal(gqlErr.Extensions[k]) - if err != nil { - fmt.Println("failed to marshal error value:", err) - } - dagErr = dagErr.WithValue(k, dagger.JSON(val)) + fnCall := dag.CurrentFunctionCall() + defer func() { + if rerr != nil { + if ` + voidRet + ` := fnCall.ReturnError(ctx, convertError(rerr)); err != nil { + fmt.Println("failed to return error:", err, "\noriginal error:", rerr) } } - return dagErr + }() + + result, err := register() + if err != nil { + var exec *dagger.ExecError + if errors.As(err, &exec) { + return exec.Unwrap() + } + return err + } + resultBytes, err := json.Marshal(result) + if err != nil { + return fmt.Errorf("marshal: %w", err) } - return dag.Error(rerr.Error()) -} -func dispatch(ctx context.Context) (rerr error) { + if ` + voidRet + ` := fnCall.ReturnValue(ctx, dagger.JSON(resultBytes)); err != nil { + return fmt.Errorf("store return value: %w", err) + } + return nil +}` + } else { + dispatch = `func dispatch(ctx context.Context) (rerr error) { ctx = telemetry.InitEmbedded(ctx, resource.NewWithAttributes( semconv.SchemaURL, semconv.ServiceNameKey.String("dagger-go-sdk"), @@ -420,10 +434,50 @@ func dispatch(ctx context.Context) (rerr error) { } return nil }` + } + + return `func main() { + ctx := context.Background() + + // Direct slog to the new stderr. This is only for dev time debugging, and + // runtime errors/warnings. + slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ + Level: slog.LevelWarn, + }))) + + if err := dispatch(ctx); err != nil { + os.Exit(2) + } +} + +func convertError(rerr error) *dagger.Error { + var gqlErr *gqlerror.Error + if errors.As(rerr, &gqlErr) { + dagErr := dag.Error(gqlErr.Message) + if gqlErr.Extensions != nil { + keys := make([]string, 0, len(gqlErr.Extensions)) + for k := range gqlErr.Extensions { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + val, err := json.Marshal(gqlErr.Extensions[k]) + if err != nil { + fmt.Println("failed to marshal error value:", err) + } + dagErr = dagErr.WithValue(k, dagger.JSON(val)) + } + } + return dagErr + } + return dag.Error(rerr.Error()) +} + +` + dispatch } // the source code of the invoke func, which is the mostly dynamically generated code that actually calls the user's functions -func invokeSrc(objFunctionCases map[string][]Code, createMod Code) string { +func invokeSrc(objFunctionCases map[string][]Code) string { // each `case` statement for every object name, which makes up the body of the invoke func objNames := []string{} for objName := range objFunctionCases { @@ -437,7 +491,7 @@ func invokeSrc(objFunctionCases map[string][]Code, createMod Code) string { } // when the object name is empty, return the module definition objCases = append(objCases, Case(Lit("")).Block( - Return(createMod, Nil()), + Return(Id(registerFuncName).Call()), )) // default case (return error) objCases = append(objCases, Default().Block( @@ -471,6 +525,21 @@ func invokeSrc(objFunctionCases map[string][]Code, createMod Code) string { return fmt.Sprintf("%#v", invokeFunc) } +// the source code of the register func, which exposes the module's defined types +func registerSrc(createMod Code) string { + // func register( + invokeFunc := Func().Id(registerFuncName).Params().Params( + // ) (_ any, + Id("_").Id("any"), + // err error) + Id("err").Error(), + ).Block( + Return(createMod, Nil()), + ) + + return fmt.Sprintf("%#v", invokeFunc) +} + // TODO: use jennifer for generating this magical typedef func (ps *parseState) renderNameOrStruct(t types.Type) string { if alias, ok := t.(*types.Alias); ok { diff --git a/cmd/codegen/main.go b/cmd/codegen/main.go index 6b583793bfe..3c6039eda97 100644 --- a/cmd/codegen/main.go +++ b/cmd/codegen/main.go @@ -29,7 +29,8 @@ var ( clientOnly bool - isInit bool + isInit bool + typeDefsOnly bool bundle bool @@ -63,6 +64,7 @@ func init() { rootCmd.Flags().StringVar(&moduleName, "module-name", "", "name of module to generate code for") rootCmd.Flags().BoolVar(&merge, "merge", false, "merge module deps with project's existing go.mod in a parent directory") rootCmd.Flags().BoolVar(&isInit, "is-init", false, "whether this command is initializing a new module") + rootCmd.Flags().BoolVar(&typeDefsOnly, "typedefs-only", false, "generate only type definitions (no client code)") rootCmd.Flags().BoolVar(&clientOnly, "client-only", false, "generate only client code") rootCmd.Flags().BoolVar(&bundle, "bundle", false, "generate the client in bundle mode") rootCmd.Flags().StringVar(&moduleSourceID, "module-source-id", "", "id of the module source to generate code for") @@ -76,12 +78,13 @@ func ClientGen(cmd *cobra.Command, args []string) error { ctx = telemetry.InitEmbedded(ctx, nil) cfg := generator.Config{ - Lang: generator.SDKLang(lang), - OutputDir: outputDir, - Merge: merge, - IsInit: isInit, - ClientOnly: clientOnly, - Bundle: bundle, + Lang: generator.SDKLang(lang), + OutputDir: outputDir, + Merge: merge, + IsInit: isInit, + TypeDefsOnly: typeDefsOnly, + ClientOnly: clientOnly, + Bundle: bundle, } // If a module source ID is provided or no introspection JSON is provided, we will query diff --git a/core/schema/modulesource.go b/core/schema/modulesource.go index 3a8e6da9a6d..85bfc9909a1 100644 --- a/core/schema/modulesource.go +++ b/core/schema/modulesource.go @@ -2010,9 +2010,9 @@ func (s *moduleSourceSchema) runModuleDefInSDK(ctx context.Context, src, srcInst return nil, ErrSDKRuntimeNotImplemented{SDK: src.Self.SDK.Source} } - // get the runtime container, which is what is exec'd when calling functions in the module - var err error - mod.Runtime, err = runtimeImpl.Runtime(ctx, mod.Deps, srcInstContentHashed) + // get the typedefs container dedicated to get the module's definition. + // this will fall back to the runtime container if `moduleTypeDefs` is not defined. + typeDefs, err := runtimeImpl.TypeDefs(ctx, mod.Deps, srcInstContentHashed) if err != nil { return nil, fmt.Errorf("failed to get module runtime: %w", err) } @@ -2048,7 +2048,7 @@ func (s *moduleSourceSchema) runModuleDefInSDK(ctx context.Context, src, srcInst ctx, mod, nil, - mod.Runtime, + typeDefs, core.NewFunction("", &core.TypeDef{ Kind: core.TypeDefKindObject, AsObject: dagql.NonNull(core.NewObjectTypeDef("Module", "")), @@ -2162,10 +2162,24 @@ func (s *moduleSourceSchema) moduleSourceAsModule( modName := src.Self.ModuleName if src.Self.SDKImpl != nil { + runtimeImpl, ok := src.Self.SDKImpl.AsRuntime() + if !ok { + return inst, ErrSDKRuntimeNotImplemented{SDK: src.Self.SDK.Source} + } + mod, err = s.runModuleDefInSDK(ctx, src, srcInstContentHashed, mod) if err != nil { return inst, err } + + // pre-load the module Runtime + if mod.Runtime == nil { + mod.Runtime, err = runtimeImpl.Runtime(ctx, mod.Deps, srcInstContentHashed) + if err != nil { + return inst, err + } + } + mod.InstanceID = dagql.CurrentID(ctx) } else { // For no SDK, provide an empty stub module definition diff --git a/core/sdk.go b/core/sdk.go index d4f77d7065a..164eb988376 100644 --- a/core/sdk.go +++ b/core/sdk.go @@ -168,6 +168,40 @@ type Runtime interface { // Current instance of the module source. dagql.Instance[*ModuleSource], ) (*Container, error) + + /* + HasModuleTypeDefs checks if the module exposes a `moduleTypeDefs` function + to be called by `TypeDefs`. + + This doesn't rely on a function exposed by the SDK, but on the list of functions + exposed. + */ + HasModuleTypeDefs() bool + + /* + TypeDefs returns a container that is used to execute module code + to retrieve the types defined by the module. + + This function will call the following exposed by the SDK: + + ```gql + moduleTypeDefs( + modSource: ModuleSource! + introspectionJSON: File! + ): Container! + ``` + + If this function is not exposed, it will fallback to `Runtime`. + */ + TypeDefs( + context.Context, + + // Current module dependencies. + *ModDeps, + + // Current instance of the module source. + dagql.Instance[*ModuleSource], + ) (*Container, error) } /* diff --git a/core/sdk/consts.go b/core/sdk/consts.go index e6266b43598..33817e21c8b 100644 --- a/core/sdk/consts.go +++ b/core/sdk/consts.go @@ -31,6 +31,7 @@ var sdkFunctions = []string{ "withConfig", "codegen", "moduleRuntime", + "moduleTypeDefs", "requiredClientGenerationFiles", "generateClient", } diff --git a/core/sdk/go_sdk.go b/core/sdk/go_sdk.go index d21dc99e338..3d6fc839030 100644 --- a/core/sdk/go_sdk.go +++ b/core/sdk/go_sdk.go @@ -37,6 +37,10 @@ type goSDK struct { rawConfig map[string]any } +func (sdk *goSDK) HasModuleTypeDefs() bool { + return true +} + type goSDKConfig struct { GoPrivate string `json:"goprivate,omitempty"` } @@ -170,7 +174,7 @@ func (sdk *goSDK) Codegen( ) (_ *core.GeneratedCode, rerr error) { ctx, span := core.Tracer(ctx).Start(ctx, "go SDK: run codegen") defer telemetry.End(span, func() error { return rerr }) - ctr, err := sdk.baseWithCodegen(ctx, deps, source) + ctr, err := sdk.baseWithCodegen(ctx, deps, source, false) if err != nil { return nil, err } @@ -206,12 +210,111 @@ func (sdk *goSDK) Codegen( }, nil } +func (sdk *goSDK) TypeDefs( + ctx context.Context, + deps *core.ModDeps, + source dagql.Instance[*core.ModuleSource], +) (_ *core.Container, rerr error) { + ctx, span := core.Tracer(ctx).Start(ctx, "go SDK: load typedefs") + defer telemetry.End(span, func() error { return rerr }) + ctr, err := sdk.baseWithCodegen(ctx, deps, source, true) + if err != nil { + return nil, err + } + if err := sdk.dag.Select(ctx, ctr, &ctr, + dagql.Selector{ + Field: "withoutUnixSocket", + Args: []dagql.NamedInput{ + { + Name: "path", + Value: dagql.NewString("/tmp/dagger-ssh-sock"), + }, + }, + }, + dagql.Selector{ + Field: "withExec", + Args: []dagql.NamedInput{ + { + Name: "args", + Value: dagql.ArrayInput[dagql.String]{ + "find", ".", + "-maxdepth", "1", + "-type", "f", + "-name", "*.go", + "!", "-name", "dagger.gen.go", + "-delete", + }, + }, + }, + }, + dagql.Selector{ + Field: "withExec", + Args: []dagql.NamedInput{ + { + Name: "args", + Value: dagql.ArrayInput[dagql.String]{ + "go", "build", + "-ldflags", "-s -w", // strip DWARF debug symbols to save a few MBs of space + "-o", goSDKRuntimePath, + ".", + }, + }, + }, + }, + dagql.Selector{ + Field: "withEntrypoint", + Args: []dagql.NamedInput{ + { + Name: "args", + Value: dagql.ArrayInput[dagql.String]{ + goSDKRuntimePath, + }, + }, + }, + }, + dagql.Selector{ + Field: "withWorkdir", + Args: []dagql.NamedInput{ + { + Name: "path", + Value: dagql.NewString(RuntimeWorkdirPath), + }, + }, + }, + // remove shared cache mounts from final container so module code can't + // do weird things with them like IPC, etc. + dagql.Selector{ + Field: "withoutMount", + Args: []dagql.NamedInput{ + { + Name: "path", + Value: dagql.String("/go/pkg/mod"), + }, + }, + }, + dagql.Selector{ + Field: "withoutMount", + Args: []dagql.NamedInput{ + { + Name: "path", + Value: dagql.String("/root/.cache/go-build"), + }, + }, + }, + ); err != nil { + return nil, fmt.Errorf("failed to build go runtime binary: %w", err) + } + return ctr.Self, nil +} + func (sdk *goSDK) Runtime( ctx context.Context, deps *core.ModDeps, source dagql.Instance[*core.ModuleSource], ) (_ *core.Container, rerr error) { - ctr, err := sdk.baseWithCodegen(ctx, deps, source) + ctx, span := core.Tracer(ctx).Start(ctx, "go SDK: load runtime") + defer telemetry.End(span, func() error { return rerr }) + ctr, err := sdk.baseWithCodegen(ctx, deps, source, false) if err != nil { return nil, err } @@ -289,6 +392,7 @@ func (sdk *goSDK) baseWithCodegen( ctx context.Context, deps *core.ModDeps, src dagql.Instance[*core.ModuleSource], + typedefsOnly bool, ) (dagql.Instance[*core.Container], error) { var ctr dagql.Instance[*core.Container] @@ -334,6 +438,10 @@ func (sdk *goSDK) baseWithCodegen( codegenArgs = append(codegenArgs, "--is-init") } + if typedefsOnly { + codegenArgs = append(codegenArgs, "--typedefs-only") + } + selectors := []dagql.Selector{ { Field: "withMountedFile", diff --git a/core/sdk/module_runtime.go b/core/sdk/module_runtime.go index f41888189a9..b88c7be8842 100644 --- a/core/sdk/module_runtime.go +++ b/core/sdk/module_runtime.go @@ -14,6 +14,31 @@ type runtimeModule struct { mod *module } +func (sdk *runtimeModule) HasModuleTypeDefs() bool { + _, ok := sdk.mod.funcs["moduleTypeDefs"] + return ok +} + +func (sdk *runtimeModule) TypeDefs( + ctx context.Context, + deps *core.ModDeps, + source dagql.Instance[*core.ModuleSource], +) (_ *core.Container, rerr error) { + if !sdk.HasModuleTypeDefs() { + return sdk.Runtime(ctx, deps, source) + } + + ctx, span := core.Tracer(ctx).Start(ctx, "module SDK: load typedefs") + defer telemetry.End(span, func() error { return rerr }) + + schemaJSONFile, err := deps.SchemaIntrospectionJSONFile(ctx, []string{"Host"}) + if err != nil { + return nil, fmt.Errorf("failed to get schema introspection json during %s module sdk runtime: %w", sdk.mod.mod.Self.Name(), err) + } + + return sdk.callModuleFn(ctx, "moduleTypeDefs", source, schemaJSONFile) +} + func (sdk *runtimeModule) Runtime( ctx context.Context, deps *core.ModDeps, @@ -26,10 +51,19 @@ func (sdk *runtimeModule) Runtime( return nil, fmt.Errorf("failed to get schema introspection json during %s module sdk runtime: %w", sdk.mod.mod.Self.Name(), err) } + return sdk.callModuleFn(ctx, "moduleRuntime", source, schemaJSONFile) +} + +func (sdk *runtimeModule) callModuleFn( + ctx context.Context, + fnName string, + source dagql.Instance[*core.ModuleSource], + schemaJSONFile dagql.Instance[*core.File], +) (_ *core.Container, rerr error) { var inst dagql.Instance[*core.Container] - err = sdk.mod.dag.Select(ctx, sdk.mod.sdk, &inst, + err := sdk.mod.dag.Select(ctx, sdk.mod.sdk, &inst, dagql.Selector{ - Field: "moduleRuntime", + Field: fnName, Args: []dagql.NamedInput{ { Name: "modSource", diff --git a/dagql/idtui/testdata/TestTelemetry/TestGolden/broken-dep/use-broken b/dagql/idtui/testdata/TestTelemetry/TestGolden/broken-dep/use-broken index ce28d9dfd26..a0afbd0c96c 100644 --- a/dagql/idtui/testdata/TestTelemetry/TestGolden/broken-dep/use-broken +++ b/dagql/idtui/testdata/TestTelemetry/TestGolden/broken-dep/use-broken @@ -5,24 +5,19 @@ Expected stderr: ├─● connecting to engine X.Xs ╰─● starting session X.Xs -▼ load module: ./viztest/broken-dep X.Xs ERROR +▼ load module: ./viztest/broken-dep X.Xs ├─● finding module configuration X.Xs -╰─▼ initializing module X.Xs ERROR - ╰─▼ ModuleSource.asModule: Module! X.Xs ERROR - ╰─▼ load dep modules X.Xs ERROR - ╰─▼ ModuleSource.asModule: Module! X.Xs ERROR - ├─▼ Container.withExec(args: ["go", "build", "-ldflags", "-s -w", "-o", "/runtime", "."]): Container! X.Xs ERROR - │ ┃ # dagger/broken - │ ┃ ./main.go:6:6: undefined: ctx - │ ! process "go build -ldflags -s -w -o /runtime ." did not complete successfully: exit code: 1 - │ - ╰─✘ asModule getModDef X.Xs ERROR - -Error logs: +├─● initializing module X.Xs +├─● inspecting module metadata X.Xs +╰─● loading type definitions X.Xs + +● parsing command line arguments X.Xs -▼ Container.withExec(args: ["go", "build", "-ldflags", "-s -w", "-o", "/runtime", "."]): Container! X.Xs ERROR -# dagger/broken -./main.go:6:6: undefined: ctx -! process "go build -ldflags -s -w -o /runtime ." did not complete successfully: exit code: 1 +● brokenDep: BrokenDep! X.Xs +▼ .useBroken: Void X.Xs ERROR +! call function "Broken": process "go build -ldflags -s -w -o /runtime ." did not complete successfully: exit code: 1 +├─● broken: Broken! X.Xs +╰─✘ .broken: Void X.Xs ERROR + ! call function "Broken": process "go build -ldflags -s -w -o /runtime ." did not complete successfully: exit code: 1 Setup tracing at https://dagger.cloud/traces/setup. To hide set DAGGER_NO_NAG=1 diff --git a/dagql/idtui/testdata/TestTelemetry/TestGolden/broken/broken b/dagql/idtui/testdata/TestTelemetry/TestGolden/broken/broken index b3ff8751a5c..38b3d767deb 100644 --- a/dagql/idtui/testdata/TestTelemetry/TestGolden/broken/broken +++ b/dagql/idtui/testdata/TestTelemetry/TestGolden/broken/broken @@ -5,22 +5,16 @@ Expected stderr: ├─● connecting to engine X.Xs ╰─● starting session X.Xs -▼ load module: ./viztest/broken-dep/broken X.Xs ERROR +▼ load module: ./viztest/broken-dep/broken X.Xs ├─● finding module configuration X.Xs -╰─▼ initializing module X.Xs ERROR - ╰─▼ ModuleSource.asModule: Module! X.Xs ERROR - ├─▼ Container.withExec(args: ["go", "build", "-ldflags", "-s -w", "-o", "/runtime", "."]): Container! X.Xs ERROR - │ ┃ # dagger/broken - │ ┃ ./main.go:6:6: undefined: ctx - │ ! process "go build -ldflags -s -w -o /runtime ." did not complete successfully: exit code: 1 - │ - ╰─✘ asModule getModDef X.Xs ERROR - -Error logs: +├─● initializing module X.Xs +├─● inspecting module metadata X.Xs +╰─● loading type definitions X.Xs + +● parsing command line arguments X.Xs -▼ Container.withExec(args: ["go", "build", "-ldflags", "-s -w", "-o", "/runtime", "."]): Container! X.Xs ERROR -# dagger/broken -./main.go:6:6: undefined: ctx -! process "go build -ldflags -s -w -o /runtime ." did not complete successfully: exit code: 1 +● broken: Broken! X.Xs +✘ .broken: Void X.Xs ERROR +! call function "Broken": process "go build -ldflags -s -w -o /runtime ." did not complete successfully: exit code: 1 Setup tracing at https://dagger.cloud/traces/setup. To hide set DAGGER_NO_NAG=1 diff --git a/sdk/java/dagger-java-annotation-processor/pom.xml b/sdk/java/dagger-java-annotation-processor/pom.xml index d70cafb1197..8c47708b3a2 100644 --- a/sdk/java/dagger-java-annotation-processor/pom.xml +++ b/sdk/java/dagger-java-annotation-processor/pom.xml @@ -76,6 +76,22 @@ --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED + + org.apache.maven.plugins + maven-shade-plugin + 3.5.0 + + + package + shade + + false + true + all + + + + diff --git a/sdk/java/dagger-java-annotation-processor/src/main/java/io/dagger/annotation/processor/DaggerModuleAnnotationProcessor.java b/sdk/java/dagger-java-annotation-processor/src/main/java/io/dagger/annotation/processor/DaggerModuleAnnotationProcessor.java index f5f72b8529c..fce215a2755 100644 --- a/sdk/java/dagger-java-annotation-processor/src/main/java/io/dagger/annotation/processor/DaggerModuleAnnotationProcessor.java +++ b/sdk/java/dagger-java-annotation-processor/src/main/java/io/dagger/annotation/processor/DaggerModuleAnnotationProcessor.java @@ -1,9 +1,5 @@ package io.dagger.annotation.processor; -import com.github.javaparser.StaticJavaParser; -import com.github.javaparser.javadoc.Javadoc; -import com.github.javaparser.javadoc.JavadocBlockTag; -import com.github.javaparser.javadoc.JavadocBlockTag.Type; import com.google.auto.service.AutoService; import com.palantir.javapoet.ClassName; import com.palantir.javapoet.CodeBlock; @@ -16,37 +12,19 @@ import io.dagger.client.FunctionCallArgValue; import io.dagger.client.JSON; import io.dagger.client.JsonConverter; -import io.dagger.client.ModuleID; -import io.dagger.client.TypeDef; import io.dagger.client.exception.DaggerExecException; -import io.dagger.client.exception.DaggerQueryException; -import io.dagger.module.annotation.Default; -import io.dagger.module.annotation.DefaultPath; -import io.dagger.module.annotation.Enum; -import io.dagger.module.annotation.Function; -import io.dagger.module.annotation.Ignore; -import io.dagger.module.annotation.Module; -import io.dagger.module.annotation.Object; -import io.dagger.module.info.EnumInfo; -import io.dagger.module.info.EnumValueInfo; -import io.dagger.module.info.FieldInfo; import io.dagger.module.info.FunctionInfo; import io.dagger.module.info.ModuleInfo; import io.dagger.module.info.ObjectInfo; -import io.dagger.module.info.ParameterInfo; -import io.dagger.module.info.TypeInfo; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.Arrays; import java.util.HashMap; -import java.util.HashSet; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.ProcessingEnvironment; @@ -55,14 +33,9 @@ import javax.annotation.processing.SupportedAnnotationTypes; import javax.annotation.processing.SupportedSourceVersion; import javax.lang.model.SourceVersion; -import javax.lang.model.element.Element; -import javax.lang.model.element.ElementKind; -import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; -import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; -import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Elements; @SupportedAnnotationTypes({ @@ -86,499 +59,153 @@ public synchronized void init(ProcessingEnvironment processingEnv) { this.elementUtils = processingEnv.getElementUtils(); // Récupération d'Elements } - ModuleInfo generateModuleInfo(Set annotations, RoundEnvironment roundEnv) { - String moduleName = System.getenv("_DAGGER_JAVA_SDK_MODULE_NAME"); - - String moduleDescription = null; - Set annotatedObjects = new HashSet<>(); - boolean hasModuleAnnotation = false; - - Map enumInfos = new HashMap<>(); - - for (TypeElement annotation : annotations) { - for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) { - switch (element.getKind()) { - case ENUM -> { - if (element.getAnnotation(Enum.class) != null) { - String qName = ((TypeElement) element).getQualifiedName().toString(); - if (!enumInfos.containsKey(qName)) { - enumInfos.put( - qName, - new EnumInfo( - element.getSimpleName().toString(), - parseJavaDocDescription(element), - element.getEnclosedElements().stream() - .filter(elt -> elt.getKind() == ElementKind.ENUM_CONSTANT) - .map( - elt -> - new EnumValueInfo( - elt.getSimpleName().toString(), - parseJavaDocDescription(elt))) - .toArray(EnumValueInfo[]::new))); - } - } - } - case PACKAGE -> { - if (hasModuleAnnotation) { - throw new IllegalStateException("Only one @Module annotation is allowed"); - } - hasModuleAnnotation = true; - moduleDescription = parseModuleDescription(element); - } - case CLASS, RECORD -> { - TypeElement typeElement = (TypeElement) element; - String qName = typeElement.getQualifiedName().toString(); - String name = typeElement.getAnnotation(Object.class).value(); - if (name.isEmpty()) { - name = typeElement.getSimpleName().toString(); - } - - boolean mainObject = areSimilar(name, moduleName); - - if (!element.getModifiers().contains(Modifier.PUBLIC)) { - throw new RuntimeException( - "The class %s must be public if annotated with @Object".formatted(qName)); - } - - boolean hasDefaultConstructor = - typeElement.getEnclosedElements().stream() - .filter(elt -> elt.getKind() == ElementKind.CONSTRUCTOR) - .map(ExecutableElement.class::cast) - .filter(constructor -> constructor.getModifiers().contains(Modifier.PUBLIC)) - .anyMatch(constructor -> constructor.getParameters().isEmpty()) - || typeElement.getEnclosedElements().stream() - .noneMatch(elt -> elt.getKind() == ElementKind.CONSTRUCTOR); - - if (!hasDefaultConstructor) { - throw new RuntimeException( - "The class %s must have a public no-argument constructor that calls super()" - .formatted(qName)); - } - - Optional constructorInfo = Optional.empty(); - if (mainObject) { - List constructorDefs = - typeElement.getEnclosedElements().stream() - .filter(elt -> elt.getKind() == ElementKind.CONSTRUCTOR) - .filter(elt -> !((ExecutableElement) elt).getParameters().isEmpty()) - .toList(); - if (constructorDefs.size() == 1) { - Element elt = constructorDefs.get(0); - constructorInfo = - Optional.of( - new FunctionInfo( - "", - "", - parseFunctionDescription(elt), - new TypeInfo( - ((ExecutableElement) elt).getReturnType().toString(), - ((ExecutableElement) elt).getReturnType().getKind().name()), - parseParameters((ExecutableElement) elt) - .toArray(new ParameterInfo[0]))); - } else if (constructorDefs.size() > 1) { - // There's more than one non-empty constructor, but Dagger only supports to expose a - // single one - throw new RuntimeException( - "The class %s must have a single non-empty constructor".formatted(qName)); - } - } - - List fieldInfoInfos = - typeElement.getEnclosedElements().stream() - .filter(elt -> elt.getKind() == ElementKind.FIELD) - .filter(elt -> !elt.getModifiers().contains(Modifier.TRANSIENT)) - .filter(elt -> !elt.getModifiers().contains(Modifier.STATIC)) - .filter(elt -> !elt.getModifiers().contains(Modifier.FINAL)) - .filter( - elt -> - elt.getModifiers().contains(Modifier.PUBLIC) - || elt.getAnnotation(Function.class) != null) - .map( - elt -> { - String fieldName = elt.getSimpleName().toString(); - TypeMirror tm = elt.asType(); - TypeKind tk = tm.getKind(); - FieldInfo f = - new FieldInfo( - fieldName, - parseSimpleDescription(elt), - new TypeInfo(tm.toString(), tk.name())); - return f; - }) - .toList(); - List functionInfos = - typeElement.getEnclosedElements().stream() - .filter(elt -> elt.getKind() == ElementKind.METHOD) - .filter(elt -> elt.getAnnotation(Function.class) != null) - .map( - elt -> { - Function moduleFunction = elt.getAnnotation(Function.class); - String fName = moduleFunction.value(); - String fqName = elt.getSimpleName().toString(); - if (fName.isEmpty()) { - fName = fqName; - } - if (!elt.getModifiers().contains(Modifier.PUBLIC)) { - throw new RuntimeException( - "The method %s#%s must be public if annotated with @Function" - .formatted(qName, fqName)); - } - - List parameterInfos = - parseParameters((ExecutableElement) elt); - - TypeMirror tm = ((ExecutableElement) elt).getReturnType(); - TypeKind tk = tm.getKind(); - FunctionInfo functionInfo = - new FunctionInfo( - fName, - fqName, - parseFunctionDescription(elt), - new TypeInfo(tm.toString(), tk.name()), - parameterInfos.toArray(new ParameterInfo[parameterInfos.size()])); - return functionInfo; - }) - .toList(); - annotatedObjects.add( - new ObjectInfo( - name, - qName, - parseObjectDescription(typeElement), - fieldInfoInfos.toArray(new FieldInfo[fieldInfoInfos.size()]), - functionInfos.toArray(new FunctionInfo[functionInfos.size()]), - constructorInfo)); - } - } + private static MethodSpec.Builder invokeFunction(ModuleInfo moduleInfo) + throws ClassNotFoundException { + var im = + MethodSpec.methodBuilder("invoke") + .addModifiers(Modifier.PRIVATE) + .returns(JSON.class) + .addException(Exception.class) + .addParameter(JSON.class, "parentJson") + .addParameter(String.class, "parentName") + .addParameter(String.class, "fnName") + .addParameter( + ParameterizedTypeName.get(Map.class, String.class, JSON.class), "inputArgs"); + var firstObj = true; + for (var objectInfo : moduleInfo.objects()) { + if (firstObj) { + firstObj = false; + im.beginControlFlow("if (parentName.equals($S))", objectInfo.name()); + } else { + im.nextControlFlow("else if (parentName.equals($S))", objectInfo.name()); } - } - - // Ensure only one single enum is defined with a specific name - Set enumSimpleNames = new HashSet<>(); - for (var enumQualifiedName : enumInfos.keySet()) { - String simpleName = enumQualifiedName.substring(enumQualifiedName.lastIndexOf('.') + 1); - if (enumSimpleNames.contains(simpleName)) { - throw new RuntimeException( - "The enum %s has already been registered via %s" - .formatted(simpleName, enumQualifiedName)); + // If there's no constructor, we can initialize the main object here as it's the same for + // all. + // But if there's a constructor we want to inline it under the function branch. + if (objectInfo.constructor().isEmpty()) { + ClassName objName = ClassName.bestGuess(objectInfo.qualifiedName()); + im.addStatement("$T clazz = Class.forName($S)", Class.class, objectInfo.qualifiedName()) + .addStatement( + "$T obj = ($T) $T.fromJSON(parentJson, clazz)", + objName, + objName, + JsonConverter.class); } - enumSimpleNames.add(simpleName); - } - - return new ModuleInfo( - moduleDescription, - annotatedObjects.toArray(new ObjectInfo[annotatedObjects.size()]), - enumInfos); - } - - private List parseParameters(ExecutableElement elt) { - return elt.getParameters().stream() - .filter(param -> !param.asType().toString().equals("io.dagger.client.Client")) - .map( - param -> { - TypeMirror tm = param.asType(); - TypeKind tk = tm.getKind(); - - boolean isOptional = false; - var optionalType = - processingEnv.getElementUtils().getTypeElement(Optional.class.getName()).asType(); - if (tm instanceof DeclaredType dt - && processingEnv.getTypeUtils().isSameType(dt.asElement().asType(), optionalType) - && !dt.getTypeArguments().isEmpty()) { - isOptional = true; - tm = dt.getTypeArguments().get(0); - tk = tm.getKind(); - } - - Default defaultAnnotation = param.getAnnotation(Default.class); - var hasDefaultAnnotation = defaultAnnotation != null; - - DefaultPath defaultPathAnnotation = param.getAnnotation(DefaultPath.class); - var hasDefaultPathAnnotation = defaultPathAnnotation != null; - - if (hasDefaultPathAnnotation - && !tm.toString().equals("io.dagger.client.Directory") - && !tm.toString().equals("io.dagger.client.File")) { - throw new IllegalArgumentException( - "Parameter " - + param.getSimpleName() - + " cannot have @DefaultPath annotation if it is not a Directory or File type"); - } - - if (hasDefaultAnnotation && hasDefaultPathAnnotation) { - throw new IllegalArgumentException( - "Parameter " - + param.getSimpleName() - + " cannot have both @Default and @DefaultPath annotations"); - } - - String defaultValue = - hasDefaultAnnotation - ? quoteIfString(defaultAnnotation.value(), tm.toString()) - : null; - - String defaultPath = hasDefaultPathAnnotation ? defaultPathAnnotation.value() : null; - - Ignore ignoreAnnotation = param.getAnnotation(Ignore.class); - var hasIgnoreAnnotation = ignoreAnnotation != null; - if (hasIgnoreAnnotation && !tm.toString().equals("io.dagger.client.Directory")) { - throw new IllegalArgumentException( - "Parameter " - + param.getSimpleName() - + " cannot have @Ignore annotation if it is not a Directory"); - } - - String[] ignoreValue = hasIgnoreAnnotation ? ignoreAnnotation.value() : null; - - String paramName = param.getSimpleName().toString(); - return new ParameterInfo( - paramName, - parseParameterDescription(elt, paramName), - new TypeInfo(tm.toString(), tk.name()), - isOptional, - Optional.ofNullable(defaultValue), - Optional.ofNullable(defaultPath), - Optional.ofNullable(ignoreValue)); - }) - .toList(); - } - - static String quoteIfString(String value, String type) { - if (value == null) { - return null; - } - if (type.equals(String.class.getName()) - && !value.equals("null") - && (!value.startsWith("\"") && !value.endsWith("\"") - || !value.startsWith("'") && !value.endsWith("'"))) { - return "\"" + value.replaceAll("\"", "\\\\\"") + "\""; - } - return value; - } - - static JavaFile generate(ModuleInfo moduleInfo) { - try { - var rm = - MethodSpec.methodBuilder("register") - .addModifiers(Modifier.PRIVATE) - .returns(ModuleID.class) - .addException(ExecutionException.class) - .addException(DaggerQueryException.class) - .addException(InterruptedException.class) - .addCode( - "$T module = $T.dag().module()", io.dagger.client.Module.class, Dagger.class); - if (isNotBlank(moduleInfo.description())) { - rm.addCode("\n .withDescription($S)", moduleInfo.description()); - } - for (var objectInfo : moduleInfo.objects()) { - rm.addCode("\n .withObject(") - .addCode("\n $T.dag().typeDef().withObject($S", Dagger.class, objectInfo.name()); - if (isNotBlank(objectInfo.description())) { - rm.addCode( - ", new $T.WithObjectArguments().withDescription($S)", - TypeDef.class, - objectInfo.description()); - } - rm.addCode(")"); // end of dag().TypeDef().withObject( - for (var fnInfo : objectInfo.functions()) { - rm.addCode("\n .withFunction(") - .addCode(withFunction(moduleInfo.enumInfos().keySet(), objectInfo, fnInfo)) - .addCode(")"); // end of .withFunction( - } - for (var fieldInfo : objectInfo.fields()) { - rm.addCode("\n .withField(") - .addCode("$S, ", fieldInfo.name()) - .addCode(DaggerType.of(fieldInfo.type()).toDaggerTypeDef()); - if (isNotBlank(fieldInfo.description())) { - rm.addCode(", new $T.WithFieldArguments()", io.dagger.client.TypeDef.class) - .addCode(".withDescription($S)", fieldInfo.description()); - } - rm.addCode(")"); - } - if (objectInfo.constructor().isPresent()) { - rm.addCode("\n .withConstructor(") - .addCode( - withFunction( - moduleInfo.enumInfos().keySet(), objectInfo, objectInfo.constructor().get())) - .addCode(")"); // end of .withConstructor - } - rm.addCode(")"); // end of .withObject( - } - for (var enumInfo : moduleInfo.enumInfos().values()) { - rm.addCode("\n .withEnum(") - .addCode("\n $T.dag().typeDef().withEnum($S", Dagger.class, enumInfo.name()); - if (isNotBlank(enumInfo.description())) { - rm.addCode( - ", new $T.WithEnumArguments().withDescription($S)", - TypeDef.class, - enumInfo.description()); - } - rm.addCode(")"); // end of dag().TypeDef().withEnum( - for (var enumValue : enumInfo.values()) { - rm.addCode("\n .withEnumValue($S", enumValue.value()); - if (isNotBlank(enumValue.description())) { - rm.addCode( - ", new $T.WithEnumValueArguments().withDescription($S)", - io.dagger.client.TypeDef.class, - enumValue.description()); - } - rm.addCode(")"); // end of .withEnumValue( + var firstFn = true; + for (var fnInfo : objectInfo.functions()) { + if (firstFn) { + firstFn = false; + im.beginControlFlow("if (fnName.equals($S))", fnInfo.name()); + } else { + im.nextControlFlow("else if (fnName.equals($S))", fnInfo.name()); } - rm.addCode(")"); // end of .withEnum( + im.addCode(functionInvoke(objectInfo, fnInfo)); } - rm.addCode(";\n") // end of module instantiation - .addStatement("return module.id()"); - var im = - MethodSpec.methodBuilder("invoke") - .addModifiers(Modifier.PRIVATE) - .returns(JSON.class) - .addException(Exception.class) - .addParameter(JSON.class, "parentJson") - .addParameter(String.class, "parentName") - .addParameter(String.class, "fnName") - .addParameter( - ParameterizedTypeName.get(Map.class, String.class, JSON.class), "inputArgs"); - var firstObj = true; - for (var objectInfo : moduleInfo.objects()) { - if (firstObj) { - firstObj = false; - im.beginControlFlow("if (parentName.equals($S))", objectInfo.name()); + if (objectInfo.constructor().isPresent()) { + if (firstFn) { + firstFn = false; + im.beginControlFlow("if (fnName.equals(\"\"))"); } else { - im.nextControlFlow("else if (parentName.equals($S))", objectInfo.name()); - } - // If there's no constructor, we can initialize the main object here as it's the same for - // all. - // But if there's a constructor we want to inline it under the function branch. - if (objectInfo.constructor().isEmpty()) { - ClassName objName = ClassName.bestGuess(objectInfo.qualifiedName()); - im.addStatement("$T clazz = Class.forName($S)", Class.class, objectInfo.qualifiedName()) - .addStatement( - "$T obj = ($T) $T.fromJSON(parentJson, clazz)", - objName, - objName, - JsonConverter.class); - } - var firstFn = true; - for (var fnInfo : objectInfo.functions()) { - if (firstFn) { - firstFn = false; - im.beginControlFlow("if (fnName.equals($S))", fnInfo.name()); - } else { - im.nextControlFlow("else if (fnName.equals($S))", fnInfo.name()); - } - im.addCode(functionInvoke(objectInfo, fnInfo)); - } - - if (objectInfo.constructor().isPresent()) { - if (firstFn) { - firstFn = false; - im.beginControlFlow("if (fnName.equals(\"\"))"); - } else { - im.nextControlFlow("if (fnName.equals(\"\"))"); - } - im.addCode(functionInvoke(objectInfo, objectInfo.constructor().get())); + im.nextControlFlow("else if (fnName.equals(\"\"))"); } + im.addCode(functionInvoke(objectInfo, objectInfo.constructor().get())); + } - if (!firstFn) { - im.endControlFlow(); // functions - } + if (!firstFn) { + im.endControlFlow(); // functions } - im.endControlFlow() // objects - .addStatement( - "throw new $T(new $T(\"unknown function \" + fnName))", - InvocationTargetException.class, - java.lang.Error.class); + } + im.endControlFlow() // objects + .addStatement( + "throw new $T(new $T(\"unknown function \" + fnName))", + InvocationTargetException.class, + java.lang.Error.class); - var f = - JavaFile.builder( - "io.dagger.gen.entrypoint", - TypeSpec.classBuilder("Entrypoint") - .addModifiers(Modifier.PUBLIC) - .addMethod(MethodSpec.constructorBuilder().build()) - .addMethod( - MethodSpec.methodBuilder("main") - .addModifiers(Modifier.PUBLIC, Modifier.STATIC) - .addException(Exception.class) - .returns(void.class) - .addParameter(String[].class, "args") - .beginControlFlow("try") - .addStatement("new Entrypoint().dispatch()") - .nextControlFlow("finally") - .addStatement("$T.dag().close()", Dagger.class) - .endControlFlow() - .build()) - .addMethod( - MethodSpec.methodBuilder("dispatch") - .addModifiers(Modifier.PRIVATE) - .returns(void.class) - .addException(Exception.class) - .addStatement( - "$T fnCall = $T.dag().currentFunctionCall()", - FunctionCall.class, - Dagger.class) - .beginControlFlow("try") - .addStatement("$T parentName = fnCall.parentName()", String.class) - .addStatement("$T fnName = fnCall.name()", String.class) - .addStatement("$T parentJson = fnCall.parent()", JSON.class) - .addStatement( - "$T fnArgs = fnCall.inputArgs()", - ParameterizedTypeName.get(List.class, FunctionCallArgValue.class)) - .addStatement( - "$T<$T, $T> inputArgs = new $T<>()", - Map.class, - String.class, - JSON.class, - HashMap.class) - .beginControlFlow( - "for ($T fnArg : fnArgs)", FunctionCallArgValue.class) - .addStatement("inputArgs.put(fnArg.name(), fnArg.value())") - .endControlFlow() - .addCode("\n") - .addStatement("$T result", JSON.class) - .beginControlFlow("if (parentName.isEmpty())") - .addStatement("$T modID = register()", ModuleID.class) - .addStatement("result = $T.toJSON(modID)", JsonConverter.class) - .nextControlFlow("else") - .addStatement( - "result = invoke(parentJson, parentName, fnName, inputArgs)") - .endControlFlow() - .addStatement("fnCall.returnValue(result)") - .nextControlFlow("catch ($T e)", InvocationTargetException.class) - .addStatement( - "fnCall.returnError($T.dag().error(e.getTargetException().getMessage()))", - Dagger.class) - .addStatement("throw e") - .nextControlFlow("catch ($T e)", DaggerExecException.class) - .addStatement( - "fnCall.returnError($T.dag().error(e.getMessage())" - + ".withValue(\"stdout\", $T.toJSON(e.getStdOut()))" - + ".withValue(\"stderr\", $T.toJSON(e.getStdErr()))" - + ".withValue(\"cmd\", $T.toJSON(e.getCmd()))" - + ".withValue(\"exitCode\", $T.toJSON(e.getExitCode()))" - + ".withValue(\"path\", $T.toJSON(e.getPath())))", - Dagger.class, - JsonConverter.class, - JsonConverter.class, - JsonConverter.class, - JsonConverter.class, - JsonConverter.class) - .addStatement("throw e") - .nextControlFlow("catch ($T e)", Exception.class) - .addStatement( - "fnCall.returnError($T.dag().error(e.getMessage()))", - Dagger.class) - .addStatement("throw e") - .endControlFlow() - .build()) - .addMethod(rm.build()) - .addMethod(im.build()) - .build()) - .addFileComment("This class has been generated by dagger-java-sdk. DO NOT EDIT.") - .indent(" ") - .addStaticImport(Dagger.class, "dag") - .build(); + return im; + } - return f; + static JavaFile generate(ModuleInfo moduleInfo) { + try { + return JavaFile.builder( + "io.dagger.gen.entrypoint", + TypeSpec.classBuilder("Entrypoint") + .addModifiers(Modifier.PUBLIC) + .addMethod(MethodSpec.constructorBuilder().build()) + .addMethod( + MethodSpec.methodBuilder("main") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addException(Exception.class) + .returns(void.class) + .addParameter(String[].class, "args") + .beginControlFlow("try") + .addStatement("new Entrypoint().dispatch()") + .nextControlFlow("finally") + .addStatement("$T.dag().close()", Dagger.class) + .endControlFlow() + .build()) + .addMethod( + MethodSpec.methodBuilder("dispatch") + .addModifiers(Modifier.PRIVATE) + .returns(void.class) + .addException(Exception.class) + .addStatement( + "$T fnCall = $T.dag().currentFunctionCall()", + FunctionCall.class, + Dagger.class) + .beginControlFlow("try") + .addStatement("$T parentName = fnCall.parentName()", String.class) + .addStatement("$T fnName = fnCall.name()", String.class) + .addStatement("$T parentJson = fnCall.parent()", JSON.class) + .addStatement( + "$T fnArgs = fnCall.inputArgs()", + ParameterizedTypeName.get(List.class, FunctionCallArgValue.class)) + .addStatement( + "$T<$T, $T> inputArgs = new $T<>()", + Map.class, + String.class, + JSON.class, + HashMap.class) + .beginControlFlow("for ($T fnArg : fnArgs)", FunctionCallArgValue.class) + .addStatement("inputArgs.put(fnArg.name(), fnArg.value())") + .endControlFlow() + .addCode("\n") + .addStatement( + "$T result = invoke(parentJson, parentName, fnName, inputArgs)", + JSON.class) + .addStatement("fnCall.returnValue(result)") + .nextControlFlow("catch ($T e)", InvocationTargetException.class) + .addStatement( + "fnCall.returnError($T.dag().error(e.getTargetException().getMessage()))", + Dagger.class) + .addStatement("throw e") + .nextControlFlow("catch ($T e)", DaggerExecException.class) + .addStatement( + "fnCall.returnError($T.dag().error(e.getMessage())" + + ".withValue(\"stdout\", $T.toJSON(e.getStdOut()))" + + ".withValue(\"stderr\", $T.toJSON(e.getStdErr()))" + + ".withValue(\"cmd\", $T.toJSON(e.getCmd()))" + + ".withValue(\"exitCode\", $T.toJSON(e.getExitCode()))" + + ".withValue(\"path\", $T.toJSON(e.getPath())))", + Dagger.class, + JsonConverter.class, + JsonConverter.class, + JsonConverter.class, + JsonConverter.class, + JsonConverter.class) + .addStatement("throw e") + .nextControlFlow("catch ($T e)", Exception.class) + .addStatement( + "fnCall.returnError($T.dag().error(e.getMessage()))", Dagger.class) + .addStatement("throw e") + .endControlFlow() + .build()) + .addMethod(invokeFunction(moduleInfo).build()) + .build()) + .addFileComment("This class has been generated by dagger-java-sdk. DO NOT EDIT.") + .indent(" ") + .addStaticImport(Dagger.class, "dag") + .build(); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } @@ -681,59 +308,9 @@ private static CodeBlock functionInvoke(ObjectInfo objectInfo, FunctionInfo fnIn return code.build(); } - public static CodeBlock withFunction( - Set enums, ObjectInfo objectInfo, FunctionInfo fnInfo) throws ClassNotFoundException { - boolean isConstructor = fnInfo.name().equals(""); - CodeBlock.Builder code = - CodeBlock.builder() - .add( - "\n $T.dag().function($S,", - Dagger.class, - isConstructor ? "" : fnInfo.name()) - .add("\n ") - .add( - isConstructor - ? DaggerType.of(objectInfo.qualifiedName()).toDaggerTypeDef() - : DaggerType.of(fnInfo.returnType()).toDaggerTypeDef()) - .add(")"); - if (isNotBlank(fnInfo.description())) { - code.add("\n .withDescription($S)", fnInfo.description()); - } - for (var parameterInfo : fnInfo.parameters()) { - code.add("\n .withArg($S, ", parameterInfo.name()) - .add(DaggerType.of(parameterInfo.type()).toDaggerTypeDef()); - if (parameterInfo.optional()) { - code.add(".withOptional(true)"); - } - boolean hasDescription = isNotBlank(parameterInfo.description()); - boolean hasDefaultValue = parameterInfo.defaultValue().isPresent(); - boolean hasDefaultPath = parameterInfo.defaultPath().isPresent(); - boolean hasIgnore = parameterInfo.ignore().isPresent(); - if (hasDescription || hasDefaultValue || hasDefaultPath || hasIgnore) { - code.add(", new $T.WithArgArguments()", io.dagger.client.Function.class); - if (hasDescription) { - code.add(".withDescription($S)", parameterInfo.description()); - } - if (hasDefaultValue) { - code.add( - ".withDefaultValue($T.from($S))", JSON.class, parameterInfo.defaultValue().get()); - } - if (hasDefaultPath) { - code.add(".withDefaultPath($S)", parameterInfo.defaultPath().get()); - } - if (hasIgnore) { - code.add(".withIgnore(").add(listOf(parameterInfo.ignore().get())).add(")"); - } - } - code.add(")"); - } - return code.build(); - } - public static TypeKind getTypeKind(String name) { try { - TypeKind kind = TypeKind.valueOf(name); - return kind; + return TypeKind.valueOf(name); } catch (IllegalArgumentException e) { return TypeKind.DECLARED; } @@ -741,7 +318,8 @@ public static TypeKind getTypeKind(String name) { @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { - ModuleInfo moduleInfo = generateModuleInfo(annotations, roundEnv); + ModuleInfo moduleInfo = + new ProcessorTools(processingEnv, elementUtils).generateModuleInfo(annotations, roundEnv); if (moduleInfo.objects().length == 0) { return true; @@ -751,7 +329,6 @@ public boolean process(Set annotations, RoundEnvironment try { JavaFile f = generate(moduleInfo); - f.writeTo(processingEnv.getFiler()); } catch (IOException e) { throw new RuntimeException(e); @@ -759,84 +336,4 @@ public boolean process(Set annotations, RoundEnvironment return true; } - - private static Boolean isNotBlank(String str) { - return str != null && !str.isBlank(); - } - - private String parseSimpleDescription(Element element) { - String javadocString = elementUtils.getDocComment(element); - if (javadocString == null) { - return ""; - } - return StaticJavaParser.parseJavadoc(javadocString).getDescription().toText().trim(); - } - - private String parseModuleDescription(Element element) { - Module annotation = element.getAnnotation(Module.class); - if (annotation != null && !annotation.description().isEmpty()) { - return annotation.description(); - } - return parseJavaDocDescription(element); - } - - private String parseObjectDescription(Element element) { - Object annotation = element.getAnnotation(Object.class); - if (annotation != null && !annotation.description().isEmpty()) { - return annotation.description(); - } - return parseJavaDocDescription(element); - } - - private String parseFunctionDescription(Element element) { - Function annotation = element.getAnnotation(Function.class); - if (annotation != null && !annotation.description().isEmpty()) { - return annotation.description(); - } - return parseJavaDocDescription(element); - } - - private String parseJavaDocDescription(Element element) { - String javadocString = elementUtils.getDocComment(element); - if (javadocString != null) { - return StaticJavaParser.parseJavadoc(javadocString).getDescription().toText().trim(); - } - return ""; - } - - private String parseParameterDescription(Element element, String paramName) { - String javadocString = elementUtils.getDocComment(element); - if (javadocString == null) { - return ""; - } - Javadoc javadoc = StaticJavaParser.parseJavadoc(javadocString); - Optional blockTag = - javadoc.getBlockTags().stream() - .filter(tag -> tag.getType() == Type.PARAM) - .filter(tag -> tag.getName().isPresent() && tag.getName().get().equals(paramName)) - .findFirst(); - return blockTag.map(tag -> tag.getContent().toText()).orElse(""); - } - - private static CodeBlock listOf(String[] array) { - return CodeBlock.builder() - .add("$T.of(", List.class) - .add(CodeBlock.join(Arrays.stream(array).map(s -> CodeBlock.of("$S", s)).toList(), ", ")) - .add(")") - .build(); - } - - private static boolean areSimilar(String str1, String str2) { - return normalize(str1).equals(normalize(str2)); - } - - private static String normalize(String str) { - if (str == null) { - return ""; - } - return str.replaceAll("[-_]", " ") // Replace kebab and snake case delimiters with spaces - .replaceAll("([a-z])([A-Z])", "$1 $2") // Split camel case words - .toLowerCase(Locale.ROOT) // Convert to lowercase - .replaceAll("\\s+", ""); // Remove all spaces - } } diff --git a/sdk/java/dagger-java-annotation-processor/src/main/java/io/dagger/annotation/processor/ProcessorTools.java b/sdk/java/dagger-java-annotation-processor/src/main/java/io/dagger/annotation/processor/ProcessorTools.java new file mode 100644 index 00000000000..d4683f5251c --- /dev/null +++ b/sdk/java/dagger-java-annotation-processor/src/main/java/io/dagger/annotation/processor/ProcessorTools.java @@ -0,0 +1,362 @@ +package io.dagger.annotation.processor; + +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.javadoc.Javadoc; +import com.github.javaparser.javadoc.JavadocBlockTag; +import io.dagger.module.annotation.*; +import io.dagger.module.annotation.Module; +import io.dagger.module.annotation.Object; +import io.dagger.module.info.*; +import java.util.*; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.element.*; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Elements; + +public class ProcessorTools { + private final ProcessingEnvironment processingEnv; + private final Elements elementUtils; + + ProcessorTools(ProcessingEnvironment processingEnv, Elements elementUtils) { + this.processingEnv = processingEnv; + this.elementUtils = elementUtils; + } + + private String quoteIfString(String value, String type) { + if (value == null) { + return null; + } + if (type.equals(String.class.getName()) + && !value.equals("null") + && (!value.startsWith("\"") && !value.endsWith("\"") + || !value.startsWith("'") && !value.endsWith("'"))) { + return "\"" + value.replaceAll("\"", "\\\\\"") + "\""; + } + return value; + } + + ModuleInfo generateModuleInfo(Set annotations, RoundEnvironment roundEnv) { + String moduleName = System.getenv("_DAGGER_JAVA_SDK_MODULE_NAME"); + + String moduleDescription = null; + Set annotatedObjects = new HashSet<>(); + boolean hasModuleAnnotation = false; + + Map enumInfos = new HashMap<>(); + + for (TypeElement annotation : annotations) { + for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) { + switch (element.getKind()) { + case ENUM -> { + if (element.getAnnotation(io.dagger.module.annotation.Enum.class) != null) { + String qName = ((TypeElement) element).getQualifiedName().toString(); + if (!enumInfos.containsKey(qName)) { + enumInfos.put( + qName, + new EnumInfo( + element.getSimpleName().toString(), + parseJavaDocDescription(element), + element.getEnclosedElements().stream() + .filter(elt -> elt.getKind() == ElementKind.ENUM_CONSTANT) + .map( + elt -> + new EnumValueInfo( + elt.getSimpleName().toString(), + parseJavaDocDescription(elt))) + .toArray(EnumValueInfo[]::new))); + } + } + } + case PACKAGE -> { + if (hasModuleAnnotation) { + throw new IllegalStateException("Only one @Module annotation is allowed"); + } + hasModuleAnnotation = true; + moduleDescription = parseModuleDescription(element); + } + case CLASS, RECORD -> { + TypeElement typeElement = (TypeElement) element; + String qName = typeElement.getQualifiedName().toString(); + String name = typeElement.getAnnotation(Object.class).value(); + if (name.isEmpty()) { + name = typeElement.getSimpleName().toString(); + } + + boolean mainObject = areSimilar(name, moduleName); + + if (!element.getModifiers().contains(Modifier.PUBLIC)) { + throw new RuntimeException( + "The class %s must be public if annotated with @Object".formatted(qName)); + } + + boolean hasDefaultConstructor = + typeElement.getEnclosedElements().stream() + .filter(elt -> elt.getKind() == ElementKind.CONSTRUCTOR) + .map(ExecutableElement.class::cast) + .filter(constructor -> constructor.getModifiers().contains(Modifier.PUBLIC)) + .anyMatch(constructor -> constructor.getParameters().isEmpty()) + || typeElement.getEnclosedElements().stream() + .noneMatch(elt -> elt.getKind() == ElementKind.CONSTRUCTOR); + + if (!hasDefaultConstructor) { + throw new RuntimeException( + "The class %s must have a public no-argument constructor that calls super()" + .formatted(qName)); + } + + Optional constructorInfo = Optional.empty(); + if (mainObject) { + List constructorDefs = + typeElement.getEnclosedElements().stream() + .filter(elt -> elt.getKind() == ElementKind.CONSTRUCTOR) + .filter(elt -> !((ExecutableElement) elt).getParameters().isEmpty()) + .toList(); + if (constructorDefs.size() == 1) { + Element elt = constructorDefs.get(0); + constructorInfo = + Optional.of( + new FunctionInfo( + "", + "", + parseFunctionDescription(elt), + new TypeInfo( + ((ExecutableElement) elt).getReturnType().toString(), + ((ExecutableElement) elt).getReturnType().getKind().name()), + parseParameters((ExecutableElement) elt) + .toArray(new ParameterInfo[0]))); + } else if (constructorDefs.size() > 1) { + // There's more than one non-empty constructor, but Dagger only supports to expose a + // single one + throw new RuntimeException( + "The class %s must have a single non-empty constructor".formatted(qName)); + } + } + + List fieldInfoInfos = + typeElement.getEnclosedElements().stream() + .filter(elt -> elt.getKind() == ElementKind.FIELD) + .filter(elt -> !elt.getModifiers().contains(Modifier.TRANSIENT)) + .filter(elt -> !elt.getModifiers().contains(Modifier.STATIC)) + .filter(elt -> !elt.getModifiers().contains(Modifier.FINAL)) + .filter( + elt -> + elt.getModifiers().contains(Modifier.PUBLIC) + || elt.getAnnotation(Function.class) != null) + .map( + elt -> { + String fieldName = elt.getSimpleName().toString(); + TypeMirror tm = elt.asType(); + TypeKind tk = tm.getKind(); + return new FieldInfo( + fieldName, + parseSimpleDescription(elt), + new TypeInfo(tm.toString(), tk.name())); + }) + .toList(); + List functionInfos = + typeElement.getEnclosedElements().stream() + .filter(elt -> elt.getKind() == ElementKind.METHOD) + .filter(elt -> elt.getAnnotation(Function.class) != null) + .map( + elt -> { + Function moduleFunction = elt.getAnnotation(Function.class); + String fName = moduleFunction.value(); + String fqName = elt.getSimpleName().toString(); + if (fName.isEmpty()) { + fName = fqName; + } + if (!elt.getModifiers().contains(Modifier.PUBLIC)) { + throw new RuntimeException( + "The method %s#%s must be public if annotated with @Function" + .formatted(qName, fqName)); + } + + List parameterInfos = + parseParameters((ExecutableElement) elt); + + TypeMirror tm = ((ExecutableElement) elt).getReturnType(); + TypeKind tk = tm.getKind(); + return new FunctionInfo( + fName, + fqName, + parseFunctionDescription(elt), + new TypeInfo(tm.toString(), tk.name()), + parameterInfos.toArray(new ParameterInfo[0])); + }) + .toList(); + annotatedObjects.add( + new ObjectInfo( + name, + qName, + parseObjectDescription(typeElement), + fieldInfoInfos.toArray(new FieldInfo[0]), + functionInfos.toArray(new FunctionInfo[0]), + constructorInfo)); + } + } + } + } + + // Ensure only one single enum is defined with a specific name + Set enumSimpleNames = new HashSet<>(); + for (var enumQualifiedName : enumInfos.keySet()) { + String simpleName = enumQualifiedName.substring(enumQualifiedName.lastIndexOf('.') + 1); + if (enumSimpleNames.contains(simpleName)) { + throw new RuntimeException( + "The enum %s has already been registered via %s" + .formatted(simpleName, enumQualifiedName)); + } + enumSimpleNames.add(simpleName); + } + + return new ModuleInfo( + moduleDescription, annotatedObjects.toArray(new ObjectInfo[0]), enumInfos); + } + + List parseParameters(ExecutableElement elt) { + return elt.getParameters().stream() + .filter(param -> !param.asType().toString().equals("io.dagger.client.Client")) + .map( + param -> { + TypeMirror tm = param.asType(); + TypeKind tk = tm.getKind(); + + boolean isOptional = false; + var optionalType = + processingEnv.getElementUtils().getTypeElement(Optional.class.getName()).asType(); + if (tm instanceof DeclaredType dt + && processingEnv.getTypeUtils().isSameType(dt.asElement().asType(), optionalType) + && !dt.getTypeArguments().isEmpty()) { + isOptional = true; + tm = dt.getTypeArguments().get(0); + tk = tm.getKind(); + } + + Default defaultAnnotation = param.getAnnotation(Default.class); + var hasDefaultAnnotation = defaultAnnotation != null; + + DefaultPath defaultPathAnnotation = param.getAnnotation(DefaultPath.class); + var hasDefaultPathAnnotation = defaultPathAnnotation != null; + + if (hasDefaultPathAnnotation + && !tm.toString().equals("io.dagger.client.Directory") + && !tm.toString().equals("io.dagger.client.File")) { + throw new IllegalArgumentException( + "Parameter " + + param.getSimpleName() + + " cannot have @DefaultPath annotation if it is not a Directory or File type"); + } + + if (hasDefaultAnnotation && hasDefaultPathAnnotation) { + throw new IllegalArgumentException( + "Parameter " + + param.getSimpleName() + + " cannot have both @Default and @DefaultPath annotations"); + } + + String defaultValue = + hasDefaultAnnotation + ? quoteIfString(defaultAnnotation.value(), tm.toString()) + : null; + + String defaultPath = hasDefaultPathAnnotation ? defaultPathAnnotation.value() : null; + + Ignore ignoreAnnotation = param.getAnnotation(Ignore.class); + var hasIgnoreAnnotation = ignoreAnnotation != null; + if (hasIgnoreAnnotation && !tm.toString().equals("io.dagger.client.Directory")) { + throw new IllegalArgumentException( + "Parameter " + + param.getSimpleName() + + " cannot have @Ignore annotation if it is not a Directory"); + } + + String[] ignoreValue = hasIgnoreAnnotation ? ignoreAnnotation.value() : null; + + String paramName = param.getSimpleName().toString(); + return new ParameterInfo( + paramName, + parseParameterDescription(elt, paramName), + new TypeInfo(tm.toString(), tk.name()), + isOptional, + Optional.ofNullable(defaultValue), + Optional.ofNullable(defaultPath), + Optional.ofNullable(ignoreValue)); + }) + .toList(); + } + + static Boolean isNotBlank(String str) { + return str != null && !str.isBlank(); + } + + private String parseSimpleDescription(Element element) { + String javadocString = elementUtils.getDocComment(element); + if (javadocString == null) { + return ""; + } + return StaticJavaParser.parseJavadoc(javadocString).getDescription().toText().trim(); + } + + private String parseModuleDescription(Element element) { + io.dagger.module.annotation.Module annotation = element.getAnnotation(Module.class); + if (annotation != null && !annotation.description().isEmpty()) { + return annotation.description(); + } + return parseJavaDocDescription(element); + } + + private String parseObjectDescription(Element element) { + Object annotation = element.getAnnotation(Object.class); + if (annotation != null && !annotation.description().isEmpty()) { + return annotation.description(); + } + return parseJavaDocDescription(element); + } + + private String parseFunctionDescription(Element element) { + Function annotation = element.getAnnotation(Function.class); + if (annotation != null && !annotation.description().isEmpty()) { + return annotation.description(); + } + return parseJavaDocDescription(element); + } + + private String parseJavaDocDescription(Element element) { + String javadocString = elementUtils.getDocComment(element); + if (javadocString != null) { + return StaticJavaParser.parseJavadoc(javadocString).getDescription().toText().trim(); + } + return ""; + } + + private String parseParameterDescription(Element element, String paramName) { + String javadocString = elementUtils.getDocComment(element); + if (javadocString == null) { + return ""; + } + Javadoc javadoc = StaticJavaParser.parseJavadoc(javadocString); + Optional blockTag = + javadoc.getBlockTags().stream() + .filter(tag -> tag.getType() == JavadocBlockTag.Type.PARAM) + .filter(tag -> tag.getName().isPresent() && tag.getName().get().equals(paramName)) + .findFirst(); + return blockTag.map(tag -> tag.getContent().toText()).orElse(""); + } + + private static boolean areSimilar(String str1, String str2) { + return normalize(str1).equals(normalize(str2)); + } + + private static String normalize(String str) { + if (str == null) { + return ""; + } + return str.replaceAll("[-_]", " ") // Replace kebab and snake case delimiters with spaces + .replaceAll("([a-z])([A-Z])", "$1 $2") // Split camel case words + .toLowerCase(Locale.ROOT) // Convert to lowercase + .replaceAll("\\s+", ""); // Remove all spaces + } +} diff --git a/sdk/java/dagger-java-annotation-processor/src/main/java/io/dagger/annotation/processor/TypeDefs.java b/sdk/java/dagger-java-annotation-processor/src/main/java/io/dagger/annotation/processor/TypeDefs.java new file mode 100644 index 00000000000..8c12bfea48e --- /dev/null +++ b/sdk/java/dagger-java-annotation-processor/src/main/java/io/dagger/annotation/processor/TypeDefs.java @@ -0,0 +1,266 @@ +package io.dagger.annotation.processor; + +import com.google.auto.service.AutoService; +import com.palantir.javapoet.*; +import io.dagger.client.*; +import io.dagger.client.exception.DaggerExecException; +import io.dagger.client.exception.DaggerQueryException; +import io.dagger.module.info.FunctionInfo; +import io.dagger.module.info.ModuleInfo; +import io.dagger.module.info.ObjectInfo; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.*; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.Processor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.*; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Elements; + +@SupportedAnnotationTypes({ + "io.dagger.module.annotation.Module", + "io.dagger.module.annotation.Object", + "io.dagger.module.annotation.Enum", + "io.dagger.module.annotation.Function", + "io.dagger.module.annotation.Optional", + "io.dagger.module.annotation.Default", + "io.dagger.module.annotation.DefaultPath" +}) +@SupportedSourceVersion(SourceVersion.RELEASE_17) +@AutoService(Processor.class) +public class TypeDefs extends AbstractProcessor { + + private Elements elementUtils; + + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + this.elementUtils = processingEnv.getElementUtils(); // Récupération d'Elements + } + + private static MethodSpec.Builder registerFunction(ModuleInfo moduleInfo) + throws ClassNotFoundException { + var rm = + MethodSpec.methodBuilder("register") + .addModifiers(Modifier.STATIC) + .returns(ModuleID.class) + .addException(ExecutionException.class) + .addException(DaggerQueryException.class) + .addException(InterruptedException.class) + .addCode("$T module = $T.dag().module()", io.dagger.client.Module.class, Dagger.class); + if (ProcessorTools.isNotBlank(moduleInfo.description())) { + rm.addCode("\n .withDescription($S)", moduleInfo.description()); + } + for (var objectInfo : moduleInfo.objects()) { + rm.addCode("\n .withObject(") + .addCode("\n $T.dag().typeDef().withObject($S", Dagger.class, objectInfo.name()); + if (ProcessorTools.isNotBlank(objectInfo.description())) { + rm.addCode( + ", new $T.WithObjectArguments().withDescription($S)", + TypeDef.class, + objectInfo.description()); + } + rm.addCode(")"); // end of dag().TypeDef().withObject( + for (var fnInfo : objectInfo.functions()) { + rm.addCode("\n .withFunction(") + .addCode(withFunction(objectInfo, fnInfo)) + .addCode(")"); // end of .withFunction( + } + for (var fieldInfo : objectInfo.fields()) { + rm.addCode("\n .withField(") + .addCode("$S, ", fieldInfo.name()) + .addCode(DaggerType.of(fieldInfo.type()).toDaggerTypeDef()); + if (ProcessorTools.isNotBlank(fieldInfo.description())) { + rm.addCode(", new $T.WithFieldArguments()", TypeDef.class) + .addCode(".withDescription($S)", fieldInfo.description()); + } + rm.addCode(")"); + } + if (objectInfo.constructor().isPresent()) { + rm.addCode("\n .withConstructor(") + .addCode(withFunction(objectInfo, objectInfo.constructor().get())) + .addCode(")"); // end of .withConstructor + } + rm.addCode(")"); // end of .withObject( + } + for (var enumInfo : moduleInfo.enumInfos().values()) { + rm.addCode("\n .withEnum(") + .addCode("\n $T.dag().typeDef().withEnum($S", Dagger.class, enumInfo.name()); + if (ProcessorTools.isNotBlank(enumInfo.description())) { + rm.addCode( + ", new $T.WithEnumArguments().withDescription($S)", + TypeDef.class, + enumInfo.description()); + } + rm.addCode(")"); // end of dag().TypeDef().withEnum( + for (var enumValue : enumInfo.values()) { + rm.addCode("\n .withEnumValue($S", enumValue.value()); + if (ProcessorTools.isNotBlank(enumValue.description())) { + rm.addCode( + ", new $T.WithEnumValueArguments().withDescription($S)", + TypeDef.class, + enumValue.description()); + } + rm.addCode(")"); // end of .withEnumValue( + } + rm.addCode(")"); // end of .withEnum( + } + rm.addCode(";\n") // end of module instantiation + .addStatement("return module.id()"); + + return rm; + } + + static JavaFile generateRegister(ModuleInfo moduleInfo) { + try { + return JavaFile.builder( + "io.dagger.gen.entrypoint", + TypeSpec.classBuilder("TypeDefs") + .addModifiers(Modifier.PUBLIC) + .addMethod(MethodSpec.constructorBuilder().build()) + .addMethod( + MethodSpec.methodBuilder("main") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addException(Exception.class) + .returns(void.class) + .addParameter(String[].class, "args") + .beginControlFlow("try") + .addStatement("new TypeDefs().dispatch()") + .nextControlFlow("finally") + .addStatement("$T.dag().close()", Dagger.class) + .endControlFlow() + .build()) + .addMethod( + MethodSpec.methodBuilder("dispatch") + .addModifiers(Modifier.PRIVATE) + .returns(void.class) + .addException(Exception.class) + .addStatement( + "$T fnCall = $T.dag().currentFunctionCall()", + FunctionCall.class, + Dagger.class) + .beginControlFlow("try") + .addStatement( + "fnCall.returnValue($T.toJSON(register()))", JsonConverter.class) + .nextControlFlow("catch ($T e)", InvocationTargetException.class) + .addStatement( + "fnCall.returnError($T.dag().error(e.getTargetException().getMessage()))", + Dagger.class) + .addStatement("throw e") + .nextControlFlow("catch ($T e)", DaggerExecException.class) + .addStatement( + "fnCall.returnError($T.dag().error(e.getMessage())" + + ".withValue(\"stdout\", $T.toJSON(e.getStdOut()))" + + ".withValue(\"stderr\", $T.toJSON(e.getStdErr()))" + + ".withValue(\"cmd\", $T.toJSON(e.getCmd()))" + + ".withValue(\"exitCode\", $T.toJSON(e.getExitCode()))" + + ".withValue(\"path\", $T.toJSON(e.getPath())))", + Dagger.class, + JsonConverter.class, + JsonConverter.class, + JsonConverter.class, + JsonConverter.class, + JsonConverter.class) + .addStatement("throw e") + .nextControlFlow("catch ($T e)", Exception.class) + .addStatement( + "fnCall.returnError($T.dag().error(e.getMessage()))", Dagger.class) + .addStatement("throw e") + .endControlFlow() + .build()) + .addMethod(registerFunction(moduleInfo).build()) + .build()) + .addFileComment("This class has been generated by dagger-java-sdk. DO NOT EDIT.") + .indent(" ") + .addStaticImport(Dagger.class, "dag") + .build(); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + public static CodeBlock withFunction(ObjectInfo objectInfo, FunctionInfo fnInfo) { + boolean isConstructor = fnInfo.name().equals(""); + CodeBlock.Builder code = + CodeBlock.builder() + .add( + "\n $T.dag().function($S,", + Dagger.class, + isConstructor ? "" : fnInfo.name()) + .add("\n ") + .add( + isConstructor + ? DaggerType.of(objectInfo.qualifiedName()).toDaggerTypeDef() + : DaggerType.of(fnInfo.returnType()).toDaggerTypeDef()) + .add(")"); + if (ProcessorTools.isNotBlank(fnInfo.description())) { + code.add("\n .withDescription($S)", fnInfo.description()); + } + for (var parameterInfo : fnInfo.parameters()) { + code.add("\n .withArg($S, ", parameterInfo.name()) + .add(DaggerType.of(parameterInfo.type()).toDaggerTypeDef()); + if (parameterInfo.optional()) { + code.add(".withOptional(true)"); + } + boolean hasDescription = ProcessorTools.isNotBlank(parameterInfo.description()); + boolean hasDefaultValue = parameterInfo.defaultValue().isPresent(); + boolean hasDefaultPath = parameterInfo.defaultPath().isPresent(); + boolean hasIgnore = parameterInfo.ignore().isPresent(); + if (hasDescription || hasDefaultValue || hasDefaultPath || hasIgnore) { + code.add(", new $T.WithArgArguments()", io.dagger.client.Function.class); + if (hasDescription) { + code.add(".withDescription($S)", parameterInfo.description()); + } + if (hasDefaultValue) { + code.add( + ".withDefaultValue($T.from($S))", JSON.class, parameterInfo.defaultValue().get()); + } + if (hasDefaultPath) { + code.add(".withDefaultPath($S)", parameterInfo.defaultPath().get()); + } + if (hasIgnore) { + code.add(".withIgnore(").add(listOf(parameterInfo.ignore().get())).add(")"); + } + } + code.add(")"); + } + return code.build(); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + ModuleInfo moduleInfo = + new ProcessorTools(processingEnv, elementUtils).generateModuleInfo(annotations, roundEnv); + + if (moduleInfo.objects().length == 0) { + return true; + } + + DaggerType.setKnownEnums(moduleInfo.enumInfos().keySet()); + + try { + JavaFile f = generateRegister(moduleInfo); + f.writeTo(processingEnv.getFiler()); + } catch (IOException e) { + throw new RuntimeException(e); + } + + return true; + } + + private static CodeBlock listOf(String[] array) { + return CodeBlock.builder() + .add("$T.of(", List.class) + .add(CodeBlock.join(Arrays.stream(array).map(s -> CodeBlock.of("$S", s)).toList(), ", ")) + .add(")") + .build(); + } +} diff --git a/sdk/java/dagger-java-annotation-processor/src/test/java/io/dagger/annotation/processor/GenerateTest.java b/sdk/java/dagger-java-annotation-processor/src/test/java/io/dagger/annotation/processor/GenerateTest.java index 1f84a8bde43..d2fce7e514e 100644 --- a/sdk/java/dagger-java-annotation-processor/src/test/java/io/dagger/annotation/processor/GenerateTest.java +++ b/sdk/java/dagger-java-annotation-processor/src/test/java/io/dagger/annotation/processor/GenerateTest.java @@ -10,7 +10,7 @@ public class GenerateTest { @Test - public void testAnnotationGeneration() throws Exception { + public void testRuntimeGeneration() throws Exception { new EnvironmentVariables("_DAGGER_JAVA_SDK_MODULE_NAME", "dagger-java") .execute( () -> { @@ -27,4 +27,23 @@ public void testAnnotationGeneration() throws Exception { JavaFileObjects.forResource("io/dagger/gen/entrypoint/Entrypoint.java")); }); } + + @Test + public void testTypeDefsGeneration() throws Exception { + new EnvironmentVariables("_DAGGER_JAVA_SDK_MODULE_NAME", "dagger-java") + .execute( + () -> { + Compilation compilation = + javac() + .withProcessors(new TypeDefs()) + .compile( + JavaFileObjects.forResource("io/dagger/java/module/DaggerJava.java"), + JavaFileObjects.forResource("io/dagger/java/module/package-info.java")); + assertThat(compilation).succeeded(); + assertThat(compilation) + .generatedSourceFile("io.dagger.gen.entrypoint.TypeDefs") + .hasSourceEquivalentTo( + JavaFileObjects.forResource("io/dagger/gen/entrypoint/typedefs.java")); + }); + } } diff --git a/sdk/java/dagger-java-annotation-processor/src/test/resources/io/dagger/gen/entrypoint/Entrypoint.java b/sdk/java/dagger-java-annotation-processor/src/test/resources/io/dagger/gen/entrypoint/Entrypoint.java index 7836fa81120..50b741b8507 100644 --- a/sdk/java/dagger-java-annotation-processor/src/test/resources/io/dagger/gen/entrypoint/Entrypoint.java +++ b/sdk/java/dagger-java-annotation-processor/src/test/resources/io/dagger/gen/entrypoint/Entrypoint.java @@ -1,40 +1,33 @@ // This class has been generated by dagger-java-sdk. DO NOT EDIT. package io.dagger.gen.entrypoint; - import static io.dagger.client.Dagger.dag; +import static io.dagger.client.Dagger.dag; - import io.dagger.client.Container; - import io.dagger.client.Directory; - import io.dagger.client.Function; - import io.dagger.client.FunctionCall; - import io.dagger.client.FunctionCallArgValue; - import io.dagger.client.JSON; - import io.dagger.client.JsonConverter; - import io.dagger.client.Module; - import io.dagger.client.ModuleID; - import io.dagger.client.Platform; - import io.dagger.client.TypeDef; - import io.dagger.client.TypeDefKind; - import io.dagger.client.exception.DaggerExecException; - import io.dagger.client.exception.DaggerQueryException; - import io.dagger.java.module.DaggerJava; - import java.lang.Class; - import java.lang.Error; - import java.lang.Exception; - import java.lang.Integer; - import java.lang.InterruptedException; - import java.lang.String; - import java.lang.reflect.InvocationTargetException; - import java.util.Arrays; - import java.util.HashMap; - import java.util.List; - import java.util.Map; - import java.util.Objects; - import java.util.Optional; - import java.util.concurrent.ExecutionException; +import io.dagger.client.Container; +import io.dagger.client.Directory; +import io.dagger.client.FunctionCall; +import io.dagger.client.FunctionCallArgValue; +import io.dagger.client.JSON; +import io.dagger.client.JsonConverter; +import io.dagger.client.Platform; +import io.dagger.client.exception.DaggerExecException; +import io.dagger.java.module.DaggerJava; +import java.lang.Class; +import java.lang.Error; +import java.lang.Exception; +import java.lang.Integer; +import java.lang.String; +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; public class Entrypoint { - Entrypoint() {} + Entrypoint() { + } public static void main(String[] args) throws Exception { try { @@ -56,13 +49,7 @@ private void dispatch() throws Exception { inputArgs.put(fnArg.name(), fnArg.value()); } - JSON result; - if (parentName.isEmpty()) { - ModuleID modID = register(); - result = JsonConverter.toJSON(modID); - } else { - result = invoke(parentJson, parentName, fnName, inputArgs); - } + JSON result = invoke(parentJson, parentName, fnName, inputArgs); fnCall.returnValue(result); } catch (InvocationTargetException e) { fnCall.returnError(dag().error(e.getTargetException().getMessage())); @@ -76,96 +63,6 @@ private void dispatch() throws Exception { } } - private ModuleID register() - throws ExecutionException, DaggerQueryException, InterruptedException { - Module module = dag().module().withDescription("Dagger Java Module example").withObject(dag() - .typeDef() - .withObject("DaggerJava", - new TypeDef.WithObjectArguments().withDescription("Dagger Java Module main object")) - .withFunction(dag().function("containerEcho", dag().typeDef().withObject("Container")) - .withDescription("Returns a container that echoes whatever string argument is provided") - .withArg("stringArg", dag().typeDef().withKind(TypeDefKind.STRING_KIND), - new Function.WithArgArguments().withDescription("string to echo") - .withDefaultValue(JSON.from("\"Hello Dagger\"")))) - .withFunction(dag().function("grepDir", dag().typeDef().withKind(TypeDefKind.STRING_KIND)) - .withDescription( - "Returns lines that match a pattern in the files of the provided Directory") - .withArg("directoryArg", dag().typeDef().withObject("Directory"), - new Function.WithArgArguments().withDescription("Directory to grep") - .withDefaultPath("sdk/java").withIgnore(List.of("**", "!*.java"))) - .withArg("pattern", - dag().typeDef().withKind(TypeDefKind.STRING_KIND).withOptional(true), - new Function.WithArgArguments() - .withDescription("Pattern to search for in the directory"))) - .withFunction(dag().function("itself", dag().typeDef().withObject("DaggerJava"))) - .withFunction(dag().function("isZero", dag().typeDef().withKind(TypeDefKind.BOOLEAN_KIND)) - .withDescription("but this description should be exposed").withArg("value", - dag().typeDef().withKind(TypeDefKind.INTEGER_KIND))) - .withFunction(dag() - .function("doThings", - dag().typeDef().withListOf(dag().typeDef().withKind(TypeDefKind.INTEGER_KIND))) - .withArg("stringArray", - dag().typeDef().withListOf(dag().typeDef().withKind(TypeDefKind.STRING_KIND))) - .withArg("ints", - dag().typeDef().withListOf(dag().typeDef().withKind(TypeDefKind.INTEGER_KIND))) - .withArg("containers", - dag().typeDef().withListOf(dag().typeDef().withObject("Container")))) - .withFunction(dag() - .function("nonNullableNoDefault", dag().typeDef().withKind(TypeDefKind.STRING_KIND)) - .withDescription("User must provide the argument") - .withArg("stringArg", dag().typeDef().withKind(TypeDefKind.STRING_KIND))) - .withFunction(dag() - .function("nonNullableDefault", dag().typeDef().withKind(TypeDefKind.STRING_KIND)) - .withDescription( - "If the user doesn't provide an argument, a default value is used. The argument can't be null.") - .withArg("stringArg", dag().typeDef().withKind(TypeDefKind.STRING_KIND), - new Function.WithArgArguments().withDefaultValue(JSON.from("\"default value\"")))) - .withFunction(dag().function("nullable", dag().typeDef().withKind(TypeDefKind.STRING_KIND)) - .withDescription( - "Make it optional but do not define a value. If the user doesn't provide an argument, it will be\n" - + " set to null.") - .withArg("stringArg", - dag().typeDef().withKind(TypeDefKind.STRING_KIND).withOptional(true))) - .withFunction(dag() - .function("nullableDefault", dag().typeDef().withKind(TypeDefKind.STRING_KIND)) - .withDescription( - "Set a default value in case the user doesn't provide a value and allow for null value.") - .withArg("stringArg", - dag().typeDef().withKind(TypeDefKind.STRING_KIND).withOptional(true), - new Function.WithArgArguments().withDefaultValue(JSON.from("\"Foo\"")))) - .withFunction(dag().function("defaultPlatform", dag().typeDef().withScalar("Platform")) - .withDescription("return the default platform as a Scalar value")) - .withFunction(dag().function("addFloat", dag().typeDef().withKind(TypeDefKind.FLOAT_KIND)) - .withArg("a", dag().typeDef().withKind(TypeDefKind.FLOAT_KIND)) - .withArg("b", dag().typeDef().withKind(TypeDefKind.FLOAT_KIND))) - .withFunction(dag() - .function("doSomething", - dag().typeDef().withKind(TypeDefKind.VOID_KIND).withOptional(true)) - .withDescription("Function returning nothing") - .withArg("src", dag().typeDef().withObject("Directory"))) - .withFunction( - dag().function("printSeverity", dag().typeDef().withKind(TypeDefKind.STRING_KIND)) - .withArg("severity", dag().typeDef().withEnum("Severity"))) - .withField("source", dag().typeDef().withObject("Directory"), - new TypeDef.WithFieldArguments().withDescription("Project source directory")) - .withField("version", dag().typeDef().withKind(TypeDefKind.STRING_KIND)) - .withConstructor(dag().function("", dag().typeDef().withObject("DaggerJava")) - .withDescription("Initialize the DaggerJava Module") - .withArg("source", dag().typeDef().withObject("Directory").withOptional(true), - new Function.WithArgArguments().withDescription("Project source directory")) - .withArg("version", dag().typeDef().withKind(TypeDefKind.STRING_KIND), - new Function.WithArgArguments().withDescription("Go version") - .withDefaultValue(JSON.from("\"1.23.2\""))))) - .withEnum(dag().typeDef() - .withEnum("Severity", new TypeDef.WithEnumArguments().withDescription("Severities")) - .withEnumValue("DEBUG", - new TypeDef.WithEnumValueArguments().withDescription("Debug severity")) - .withEnumValue("INFO", - new TypeDef.WithEnumValueArguments().withDescription("Info severity")) - .withEnumValue("WARN").withEnumValue("ERROR").withEnumValue("FATAL")); - return module.id(); - } - private JSON invoke(JSON parentJson, String parentName, String fnName, Map inputArgs) throws Exception { if (parentName.equals("DaggerJava")) { @@ -223,8 +120,7 @@ private JSON invoke(JSON parentJson, String parentName, String fnName, Objects.requireNonNull(ints, "ints must not be null"); List containers = null; if (inputArgs.get("containers") != null) { - containers = - Arrays.asList(JsonConverter.fromJSON(inputArgs.get("containers"), Container[].class)); + containers = Arrays.asList(JsonConverter.fromJSON(inputArgs.get("containers"), Container[].class)); } Objects.requireNonNull(containers, "containers must not be null"); int[] res = obj.doThings(stringArray, ints, containers); @@ -307,8 +203,7 @@ private JSON invoke(JSON parentJson, String parentName, String fnName, Objects.requireNonNull(severity, "severity must not be null"); String res = obj.printSeverity(severity); return JsonConverter.toJSON(res); - } - if (fnName.equals("")) { + } else if (fnName.equals("")) { Directory source = null; if (inputArgs.get("source") != null) { source = JsonConverter.fromJSON(inputArgs.get("source"), Directory.class); @@ -325,4 +220,4 @@ private JSON invoke(JSON parentJson, String parentName, String fnName, } throw new InvocationTargetException(new Error("unknown function " + fnName)); } -} +} \ No newline at end of file diff --git a/sdk/java/dagger-java-annotation-processor/src/test/resources/io/dagger/gen/entrypoint/typedefs.java b/sdk/java/dagger-java-annotation-processor/src/test/resources/io/dagger/gen/entrypoint/typedefs.java new file mode 100644 index 00000000000..0a34a0747f6 --- /dev/null +++ b/sdk/java/dagger-java-annotation-processor/src/test/resources/io/dagger/gen/entrypoint/typedefs.java @@ -0,0 +1,137 @@ +// This class has been generated by dagger-java-sdk. DO NOT EDIT. +package io.dagger.gen.entrypoint; + +import static io.dagger.client.Dagger.dag; + +import io.dagger.client.Function; +import io.dagger.client.FunctionCall; +import io.dagger.client.JSON; +import io.dagger.client.JsonConverter; +import io.dagger.client.Module; +import io.dagger.client.ModuleID; +import io.dagger.client.TypeDef; +import io.dagger.client.TypeDefKind; +import io.dagger.client.exception.DaggerExecException; +import io.dagger.client.exception.DaggerQueryException; +import java.lang.Exception; +import java.lang.InterruptedException; +import java.lang.String; +import java.lang.reflect.InvocationTargetException; +import java.util.List; +import java.util.concurrent.ExecutionException; + +public class TypeDefs { + TypeDefs() { + } + + public static void main(String[] args) throws Exception { + try { + new TypeDefs().dispatch(); + } finally { + dag().close(); + } + } + + private void dispatch() throws Exception { + FunctionCall fnCall = dag().currentFunctionCall(); + try { + fnCall.returnValue(JsonConverter.toJSON(register())); + } catch (InvocationTargetException e) { + fnCall.returnError(dag().error(e.getTargetException().getMessage())); + throw e; + } catch (DaggerExecException e) { + fnCall.returnError(dag().error(e.getMessage()).withValue("stdout", JsonConverter.toJSON(e.getStdOut())).withValue("stderr", JsonConverter.toJSON(e.getStdErr())).withValue("cmd", JsonConverter.toJSON(e.getCmd())).withValue("exitCode", JsonConverter.toJSON(e.getExitCode())).withValue("path", JsonConverter.toJSON(e.getPath()))); + throw e; + } catch (Exception e) { + fnCall.returnError(dag().error(e.getMessage())); + throw e; + } + } + + static ModuleID register() throws ExecutionException, DaggerQueryException, InterruptedException { + Module module = dag().module() + .withDescription("Dagger Java Module example") + .withObject( + dag().typeDef().withObject("DaggerJava", new TypeDef.WithObjectArguments().withDescription("Dagger Java Module main object")) + .withFunction( + dag().function("containerEcho", + dag().typeDef().withObject("Container")) + .withDescription("Returns a container that echoes whatever string argument is provided") + .withArg("stringArg", dag().typeDef().withKind(TypeDefKind.STRING_KIND), new Function.WithArgArguments().withDescription("string to echo").withDefaultValue(JSON.from("\"Hello Dagger\"")))) + .withFunction( + dag().function("grepDir", + dag().typeDef().withKind(TypeDefKind.STRING_KIND)) + .withDescription("Returns lines that match a pattern in the files of the provided Directory") + .withArg("directoryArg", dag().typeDef().withObject("Directory"), new Function.WithArgArguments().withDescription("Directory to grep").withDefaultPath("sdk/java").withIgnore(List.of("**", "!*.java"))) + .withArg("pattern", dag().typeDef().withKind(TypeDefKind.STRING_KIND).withOptional(true), new Function.WithArgArguments().withDescription("Pattern to search for in the directory"))) + .withFunction( + dag().function("itself", + dag().typeDef().withObject("DaggerJava"))) + .withFunction( + dag().function("isZero", + dag().typeDef().withKind(TypeDefKind.BOOLEAN_KIND)) + .withDescription("but this description should be exposed") + .withArg("value", dag().typeDef().withKind(TypeDefKind.INTEGER_KIND))) + .withFunction( + dag().function("doThings", + dag().typeDef().withListOf(dag().typeDef().withKind(TypeDefKind.INTEGER_KIND))) + .withArg("stringArray", dag().typeDef().withListOf(dag().typeDef().withKind(TypeDefKind.STRING_KIND))) + .withArg("ints", dag().typeDef().withListOf(dag().typeDef().withKind(TypeDefKind.INTEGER_KIND))) + .withArg("containers", dag().typeDef().withListOf(dag().typeDef().withObject("Container")))) + .withFunction( + dag().function("nonNullableNoDefault", + dag().typeDef().withKind(TypeDefKind.STRING_KIND)) + .withDescription("User must provide the argument") + .withArg("stringArg", dag().typeDef().withKind(TypeDefKind.STRING_KIND))) + .withFunction( + dag().function("nonNullableDefault", + dag().typeDef().withKind(TypeDefKind.STRING_KIND)) + .withDescription("If the user doesn't provide an argument, a default value is used. The argument can't be null.") + .withArg("stringArg", dag().typeDef().withKind(TypeDefKind.STRING_KIND), new Function.WithArgArguments().withDefaultValue(JSON.from("\"default value\"")))) + .withFunction( + dag().function("nullable", + dag().typeDef().withKind(TypeDefKind.STRING_KIND)) + .withDescription("Make it optional but do not define a value. If the user doesn't provide an argument, it will be\n" + + " set to null.") + .withArg("stringArg", dag().typeDef().withKind(TypeDefKind.STRING_KIND).withOptional(true))) + .withFunction( + dag().function("nullableDefault", + dag().typeDef().withKind(TypeDefKind.STRING_KIND)) + .withDescription("Set a default value in case the user doesn't provide a value and allow for null value.") + .withArg("stringArg", dag().typeDef().withKind(TypeDefKind.STRING_KIND).withOptional(true), new Function.WithArgArguments().withDefaultValue(JSON.from("\"Foo\"")))) + .withFunction( + dag().function("defaultPlatform", + dag().typeDef().withScalar("Platform")) + .withDescription("return the default platform as a Scalar value")) + .withFunction( + dag().function("addFloat", + dag().typeDef().withKind(TypeDefKind.FLOAT_KIND)) + .withArg("a", dag().typeDef().withKind(TypeDefKind.FLOAT_KIND)) + .withArg("b", dag().typeDef().withKind(TypeDefKind.FLOAT_KIND))) + .withFunction( + dag().function("doSomething", + dag().typeDef().withKind(TypeDefKind.VOID_KIND).withOptional(true)) + .withDescription("Function returning nothing") + .withArg("src", dag().typeDef().withObject("Directory"))) + .withFunction( + dag().function("printSeverity", + dag().typeDef().withKind(TypeDefKind.STRING_KIND)) + .withArg("severity", dag().typeDef().withEnum("Severity"))) + .withField("source", dag().typeDef().withObject("Directory"), new TypeDef.WithFieldArguments().withDescription("Project source directory")) + .withField("version", dag().typeDef().withKind(TypeDefKind.STRING_KIND)) + .withConstructor( + dag().function("", + dag().typeDef().withObject("DaggerJava")) + .withDescription("Initialize the DaggerJava Module") + .withArg("source", dag().typeDef().withObject("Directory").withOptional(true), new Function.WithArgArguments().withDescription("Project source directory")) + .withArg("version", dag().typeDef().withKind(TypeDefKind.STRING_KIND), new Function.WithArgArguments().withDescription("Go version").withDefaultValue(JSON.from("\"1.23.2\""))))) + .withEnum( + dag().typeDef().withEnum("Severity", new TypeDef.WithEnumArguments().withDescription("Severities")) + .withEnumValue("DEBUG", new TypeDef.WithEnumValueArguments().withDescription("Debug severity")) + .withEnumValue("INFO", new TypeDef.WithEnumValueArguments().withDescription("Info severity")) + .withEnumValue("WARN") + .withEnumValue("ERROR") + .withEnumValue("FATAL")); + return module.id(); + } +} \ No newline at end of file diff --git a/sdk/java/runtime/main.go b/sdk/java/runtime/main.go index 784943d26be..4940aa82d91 100644 --- a/sdk/java/runtime/main.go +++ b/sdk/java/runtime/main.go @@ -35,9 +35,14 @@ type JavaSdk struct { } type moduleConfig struct { - name string - subPath string - dirPath string + name string + subPath string + dirPath string + pkgName string + kebabName string + camelName string + version string + moduleVersion string } func (c *moduleConfig) modulePath() string { @@ -77,7 +82,7 @@ func (m *JavaSdk) Codegen( modSource *dagger.ModuleSource, introspectionJSON *dagger.File, ) (*dagger.GeneratedCode, error) { - if err := m.setModuleConfig(ctx, modSource); err != nil { + if err := m.setModuleConfig(ctx, modSource, introspectionJSON); err != nil { return nil, err } mvnCtr, err := m.codegenBase(ctx, modSource, introspectionJSON) @@ -100,8 +105,8 @@ func (m *JavaSdk) Codegen( }), nil } -// codegenBase takes the user module code, add the generated SDK dependencies -// if the user module code is empty, creates a default module content based on the template from the SDK +// codegenBase takes the user module code, add the generated SDK dependencies. +// If the user module code is empty, creates a default module content based on the template from the SDK // The generated container will *not* contain the SDK source code, but only the packages built from the SDK func (m *JavaSdk) codegenBase( ctx context.Context, @@ -122,12 +127,6 @@ func (m *JavaSdk) codegenBase( if err != nil { return nil, err } - // Ensure the version in the pom.xml is the same as the introspection file - // This is updating the pom.xml whatever it's coming from the template or the user module - version, err := m.getDaggerVersionForModule(ctx, introspectionJSON) - if err != nil { - return nil, err - } ctr = ctr. // set the version of the Dagger dependencies to the version of the introspection file WithExec(m.mavenCommand( @@ -135,7 +134,7 @@ func (m *JavaSdk) codegenBase( "versions:set-property", "-DgenerateBackupPoms=false", "-Dproperty=dagger.module.deps", - fmt.Sprintf("-DnewVersion=%s", version), + fmt.Sprintf("-DnewVersion=%s", m.moduleConfig.moduleVersion), )) return ctr, nil } @@ -152,24 +151,41 @@ func (m *JavaSdk) buildJavaDependencies( if err != nil { return nil, err } - version, err := m.getDaggerVersionForModule(ctx, introspectionJSON) - if err != nil { - return nil, err - } - return ctr. + ctr = ctr. // Cache maven dependencies WithMountedCache("/root/.m2", dag.CacheVolume("sdk-java-maven-m2"), dagger.ContainerWithMountedCacheOpts{Sharing: dagger.CacheSharingModeLocked}). - // Mount the introspection JSON file used to generate the SDK - WithMountedFile("/schema.json", introspectionJSON). // Copy the SDK source directory, so all the files needed to build the dependencies WithDirectory(GenPath, m.SDKSourceDir). - WithWorkdir(GenPath). + WithWorkdir(GenPath) + // build the SDK and tools if required + for _, project := range []string{ + "dagger-codegen-maven-plugin", + "dagger-java-sdk", + "dagger-java-annotation-processor", + } { + if exitCode, _ := ctr.WithExec( + m.mavenCommand( + "mvn", "-o", "dependency:get", + fmt.Sprintf("-Dartifact=io.dagger:%s:%s", project, m.moduleConfig.version)), + dagger.ContainerWithExecOpts{Expect: dagger.ReturnTypeAny}).ExitCode(ctx); exitCode != 0 { + _, err = ctr.WithExec(m.mavenCommand( + "mvn", "--projects", "dagger-codegen-maven-plugin", "install", "-DskipTests", + )).ExitCode(ctx) + if err != nil { + return nil, err + } + } + } + return ctr. + // Mount the introspection JSON file used to generate the SDK + WithMountedFile("/schema.json", introspectionJSON). + WithExec([]string{"cat", "/schema.json"}). // Set the version of the dependencies we are building to the version of the introspection file WithExec(m.mavenCommand( "mvn", "versions:set", "-DgenerateBackupPoms=false", - fmt.Sprintf("-DnewVersion=%s", version), + fmt.Sprintf("-DnewVersion=%s", m.moduleConfig.moduleVersion), )). // Build and install the java modules one by one // - dagger-codegen-maven-plugin: this plugin will be used to generate the SDK code, from the introspection file, @@ -196,11 +212,6 @@ func (m *JavaSdk) addTemplate( ctx context.Context, ctr *dagger.Container, ) (*dagger.Container, error) { - name := m.moduleConfig.name - pkgName := strings.ReplaceAll(strings.ReplaceAll(strings.ToLower(name), "-", ""), "_", "") - kebabName := strcase.ToKebab(name) - camelName := strcase.ToCamel(name) - // Check if there's a pom.xml inside the module path. If a file exist, no need to add the templates if _, err := ctr.File(filepath.Join(m.moduleConfig.modulePath(), "pom.xml")).Name(ctx); err == nil { return ctr, nil @@ -211,8 +222,9 @@ func (m *JavaSdk) addTemplate( } changes := []repl{ - {"dagger-module-placeholder", kebabName}, - {"daggermoduleplaceholder", pkgName}, + {"dagger-module-placeholder", m.moduleConfig.kebabName}, + {"dagger-module-typedefs-placeholder", m.moduleConfig.kebabName + "-typedefs"}, + {"daggermoduleplaceholder", m.moduleConfig.pkgName}, } // Edit template content so that they match the dagger module name @@ -223,7 +235,7 @@ func (m *JavaSdk) addTemplate( return ctr, fmt.Errorf("could not add template: %w", err) } - changes = append(changes, repl{"DaggerModule", camelName}) + changes = append(changes, repl{"DaggerModule", m.moduleConfig.camelName}) daggerModuleJava, err := m.replace(ctx, templateDir, filepath.Join("src", "main", "java", "io", "dagger", "modules", "daggermodule", "DaggerModule.java"), changes...) @@ -240,8 +252,8 @@ func (m *JavaSdk) addTemplate( // And copy them to the container, renamed to match the dagger module name ctr = ctr. WithNewFile(absPath("pom.xml"), pomXML). - WithNewFile(absPath("src", "main", "java", "io", "dagger", "modules", pkgName, fmt.Sprintf("%s.java", camelName)), daggerModuleJava). - WithNewFile(absPath("src", "main", "java", "io", "dagger", "modules", pkgName, "package-info.java"), packageInfoJava) + WithNewFile(absPath("src", "main", "java", "io", "dagger", "modules", m.moduleConfig.pkgName, fmt.Sprintf("%s.java", m.moduleConfig.camelName)), daggerModuleJava). + WithNewFile(absPath("src", "main", "java", "io", "dagger", "modules", m.moduleConfig.pkgName, "package-info.java"), packageInfoJava) return ctr, nil } @@ -292,12 +304,44 @@ func (m *JavaSdk) generateCode( Directory(ModSourceDirPath), nil } +func (m *JavaSdk) ModuleTypeDefs( + ctx context.Context, + modSource *dagger.ModuleSource, + introspectionJSON *dagger.File, +) (*dagger.Container, error) { + if err := m.setModuleConfig(ctx, modSource, introspectionJSON); err != nil { + return nil, err + } + + // Get a container with the user module sources and the SDK packages built and installed + mvnCtr, err := m.codegenBase(ctx, modSource, introspectionJSON) + if err != nil { + return nil, err + } + // Build the executable jar + jar, err := m.buildTypeDefsJar(ctx, mvnCtr) + if err != nil { + return nil, err + } + + javaCtr, err := m.jreContainer(ctx) + if err != nil { + return nil, err + } + javaCtr = javaCtr. + WithFile(filepath.Join(ModDirPath, "module.jar"), jar). + WithWorkdir(ModDirPath). + WithEntrypoint([]string{"java", "-jar", filepath.Join(ModDirPath, "module.jar")}) + + return javaCtr, nil +} + func (m *JavaSdk) ModuleRuntime( ctx context.Context, modSource *dagger.ModuleSource, introspectionJSON *dagger.File, ) (*dagger.Container, error) { - if err := m.setModuleConfig(ctx, modSource); err != nil { + if err := m.setModuleConfig(ctx, modSource, introspectionJSON); err != nil { return nil, err } @@ -339,7 +383,59 @@ func (m *JavaSdk) buildJar( "clean", "package", "-DskipTests", - ))) + )), + m.moduleConfig.modulePath()) +} + +// buildTypeDefsJar builds and returns the generated jar to register types +func (m *JavaSdk) buildTypeDefsJar( + ctx context.Context, + ctr *dagger.Container, +) (*dagger.File, error) { + out, err := ctr.WithExec([]string{"find", "src/main/java", "-name", "*.java"}).Stdout(ctx) + if err != nil { + return nil, err + } + + templateDir := dag.CurrentModule().Source().Directory("template") + typeDefsPomXML, err := m.replace(ctx, templateDir, + "typedefs/pom.xml", + repl{"dagger-module-typedefs-placeholder", m.moduleConfig.kebabName + "-typedefs"}, + ) + if err != nil { + return nil, err + } + + return m.finalJar(ctx, + ctr. + WithExec(append([]string{ + "javac", + "-cp", fmt.Sprintf("/root/.m2/repository/io/dagger/dagger-java-annotation-processor/%[1]s/dagger-java-annotation-processor-%[1]s-all.jar", m.moduleConfig.moduleVersion), + "-processor", "io.dagger.annotation.processor.TypeDefs", + "-proc:only", + "-d", "target/generated", + }, strings.Split(strings.TrimSpace(out), "\n")...)). + WithWorkdir(filepath.Join(m.moduleConfig.modulePath(), "typedefs")). + WithExec([]string{"mkdir", "-p", "src/main/java/io/dagger/gen/entrypoint"}). + WithExec([]string{ + "cp", + "../target/generated/io/dagger/gen/entrypoint/TypeDefs.java", + "src/main/java/io/dagger/gen/entrypoint/TypeDefs.java", + }). + WithNewFile("pom.xml", typeDefsPomXML). + WithExec(m.mavenCommand( + "mvn", + "versions:set-property", + "-DgenerateBackupPoms=false", + "-Dproperty=dagger.module.deps", + fmt.Sprintf("-DnewVersion=%s", m.moduleConfig.moduleVersion), + )). + WithExec(m.mavenCommand( + "mvn", + "clean", + "package", + "-DskipTests")), + filepath.Join(m.moduleConfig.modulePath(), "typedefs")) } // finalJar will return the jar corresponding to the user module built @@ -350,6 +446,7 @@ func (m *JavaSdk) buildJar( func (m *JavaSdk) finalJar( ctx context.Context, ctr *dagger.Container, + rootDir string, ) (*dagger.File, error) { artifactID, err := ctr. WithExec(m.mavenCommand("mvn", "org.apache.maven.plugins:maven-help-plugin:3.2.0:evaluate", "-Dexpression=project.artifactId", "-q", "-DforceStdout")). @@ -365,7 +462,7 @@ func (m *JavaSdk) finalJar( } jarFileName := fmt.Sprintf("%s-%s.jar", artifactID, version) - return ctr.File(filepath.Join(m.moduleConfig.modulePath(), "target", jarFileName)), nil + return ctr.File(filepath.Join(rootDir, "target", jarFileName)), nil } func (m *JavaSdk) mvnContainer(ctx context.Context) (*dagger.Container, error) { @@ -391,11 +488,14 @@ func disableSVEOnArm64(ctx context.Context, ctr *dagger.Container) (*dagger.Cont return ctr, nil } -func (m *JavaSdk) setModuleConfig(ctx context.Context, modSource *dagger.ModuleSource) error { +func (m *JavaSdk) setModuleConfig(ctx context.Context, modSource *dagger.ModuleSource, introspectionJSON *dagger.File) error { modName, err := modSource.ModuleName(ctx) if err != nil { return err } + pkgName := strings.ReplaceAll(strings.ReplaceAll(strings.ToLower(modName), "-", ""), "_", "") + kebabName := strcase.ToKebab(modName) + camelName := strcase.ToCamel(modName) subPath, err := modSource.SourceSubpath(ctx) if err != nil { return err @@ -404,16 +504,26 @@ func (m *JavaSdk) setModuleConfig(ctx context.Context, modSource *dagger.ModuleS if err != nil { return err } + version, err := m.getDaggerVersion(ctx, introspectionJSON) + if err != nil { + return err + } + moduleVersion := m.getDaggerVersionForModule(version) m.moduleConfig = moduleConfig{ - name: modName, - subPath: subPath, - dirPath: dirPath, + name: modName, + pkgName: pkgName, + kebabName: kebabName, + camelName: camelName, + subPath: subPath, + dirPath: dirPath, + version: version, + moduleVersion: moduleVersion, } return nil } -func (m *JavaSdk) getDaggerVersionForModule(ctx context.Context, introspectionJSON *dagger.File) (string, error) { +func (m *JavaSdk) getDaggerVersion(ctx context.Context, introspectionJSON *dagger.File) (string, error) { content, err := introspectionJSON.Contents(ctx) if err != nil { return "", err @@ -422,11 +532,15 @@ func (m *JavaSdk) getDaggerVersionForModule(ctx context.Context, introspectionJS if err = json.Unmarshal([]byte(content), &introspectJSON); err != nil { return "", err } + return strings.TrimPrefix(introspectJSON.SchemaVersion, "v"), nil +} + +func (m *JavaSdk) getDaggerVersionForModule(version string) string { return fmt.Sprintf( "%s-%s-module", - strings.TrimPrefix(introspectJSON.SchemaVersion, "v"), + version, m.moduleConfig.name, - ), nil + ) } type IntrospectJSON struct { diff --git a/sdk/java/runtime/template/typedefs/pom.xml b/sdk/java/runtime/template/typedefs/pom.xml new file mode 100644 index 00000000000..f2ec5f5bc07 --- /dev/null +++ b/sdk/java/runtime/template/typedefs/pom.xml @@ -0,0 +1,116 @@ + + + 4.0.0 + + io.dagger.modules.daggermodule + dagger-module-typedefs-placeholder + 1.0-SNAPSHOT + dagger-module-typedefs-placeholder + + + 17 + UTF-8 + 0.18.8-template-module + + + + + io.dagger + dagger-java-sdk + ${dagger.module.deps} + + + org.slf4j + slf4j-simple + runtime + 2.0.16 + + + org.eclipse + yasson + 3.0.4 + runtime + + + io.netty + netty-handler + 4.1.118.Final + + + net.minidev + json-smart + 2.5.2 + runtime + + + + + + + + maven-clean-plugin + 3.4.0 + + + maven-resources-plugin + 3.3.1 + + + maven-compiler-plugin + 3.13.0 + + + maven-surefire-plugin + 3.3.0 + + + maven-jar-plugin + 3.4.2 + + + maven-install-plugin + 3.1.2 + + + maven-deploy-plugin + 3.1.2 + + + maven-site-plugin + 3.12.1 + + + maven-project-info-reports-plugin + 3.6.1 + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.0 + + + package + + shade + + + + + io.dagger.gen.entrypoint.TypeDefs + + + ${project.build.outputDirectory} + ${project.artifactId}-${project.version} + + + + + + + diff --git a/sdk/java/runtime/template/typedefs/src/main/java/io/dagger/gen/entrypoint/.gitkeep b/sdk/java/runtime/template/typedefs/src/main/java/io/dagger/gen/entrypoint/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d