Skip to content

Commit

Permalink
feat: add tasks for OpenAPI docs and api-client code generation (#16)
Browse files Browse the repository at this point in the history
### What this PR does?
实现了两个 tasks,用于为插件项目生成 OpenAPI docs 和 API Client
- `generateOpenApiDocs`: 启动一个 Halo 作为 API Docs Server 并根据用户配置的 groupedApiMappings 来下载 Open API 的 json 文件到指定目录
- `generateApiClient` 根据 groupedApiMappings 指定的文件名称和其他配置来生成 API Client 到前端模块的项目中

**使用方式:**
在 build.gradle 中配置

```kotlin
haloPlugin {
    // 可以配置 configurationPropertiesFile 来指定插件需要的配置文件如应用市场插件
    // configurationPropertiesFile = file("${projectDir}/test.yaml")
    openApi {
        outputDir = file("$rootDir/api-docs/openapi/v3_0")
        // 用于定义 API 分组规则
         groupingRules {
            // 此名称为 group name,定义后 groupedApiMappings 中的 /v3/api-docs/ 后的名称需要与之相同,要避免与 halo 中已经存在的 group 相同避免生成后出现与插件无关的 API
            staticPagesExtensionV1alpha1Api {
                // 分组显示名称
                displayName = 'Extension API for Static Pages'
                // 分组的 API 规则
                pathsToMatch = ['/apis/staticpage.halo.run/v1alpha1/**']
            }
        }
        groupedApiMappings = [
                '/v3/api-docs/staticPagesExtensionV1alpha1Api': 'staticPagesExtensionV1alpha1Api.json'
        ]
        generator {
            // 默认配置可缺省
            outputDir = file("${projectDir}/console/src/api/generated")
            // 默认配置可缺省
            additionalProperties = [
                    useES6: true,
                    useSingleRequestParameter: true,
                    withSeparateModelsAndApi: true,
                    apiPackage: "api",
                    modelPackage: "models"
            ]
            // 默认配置可缺省
            typeMappings = [
                    set: "Array"
            ]
        }
    }
}
```

**how to test it?**
1. `./gradlew clean build -x check `
2. `./gradlew publishToMavenLocal`
3. 在插件项目中使用此插件 `id "run.halo.plugin.devtools" version "0.0.10-SNAPSHOT"`
4. 配置插件的 `settings.gradle` 中的 repositories 添加 `mavenLocal()`

```release-note
支持生成 API 文档(需升级 Gradle 至 8.3 及以上的版本)
```
  • Loading branch information
guqing authored Aug 1, 2024
1 parent 821f7aa commit 5f2e4be
Show file tree
Hide file tree
Showing 17 changed files with 1,066 additions and 34 deletions.
21 changes: 19 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ plugins {
}

group 'run.halo.gradle'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}

sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17

repositories {
mavenCentral()
Expand Down Expand Up @@ -58,11 +65,21 @@ ext {
asm = '9.4'
dockerJava = '3.3.0'
jsonPatch = '1.13'
openApiGenerator = '7.7.0'
}

dependencies {
implementation "com.github.docker-java:docker-java-transport-httpclient5:$dockerJava"
implementation "com.github.docker-java:docker-java-core:$dockerJava"
implementation("com.github.docker-java:docker-java-transport-httpclient5:$dockerJava") {
exclude group: 'org.slf4j'
}
implementation("com.github.docker-java:docker-java-core:$dockerJava") {
exclude group: 'org.slf4j'
exclude group: 'com.fasterxml.jackson.core'
}

implementation("org.openapitools:openapi-generator:$openApiGenerator") {
exclude group: 'org.slf4j'
}

implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$jackjson"
implementation "com.fasterxml.jackson.core:jackson-databind:$jackjson"
Expand Down
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
4 changes: 3 additions & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
41 changes: 28 additions & 13 deletions gradlew
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
Expand All @@ -80,13 +80,11 @@ do
esac
done

APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit

APP_NAME="Gradle"
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}

# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit

# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
Expand Down Expand Up @@ -133,22 +131,29 @@ location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi

# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
Expand Down Expand Up @@ -193,18 +198,28 @@ if "$cygwin" || "$msys" ; then
done
fi

# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.

# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'

# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.

set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"

# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi

# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
Expand Down
35 changes: 19 additions & 16 deletions gradlew.bat
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
@rem limitations under the License.
@rem

@if "%DEBUG%" == "" @echo off
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
Expand All @@ -25,7 +25,8 @@
if "%OS%"=="Windows_NT" setlocal

set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%

Expand All @@ -40,13 +41,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome

set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
if %ERRORLEVEL% equ 0 goto execute

echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2

goto fail

Expand All @@ -56,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe

if exist "%JAVA_EXE%" goto execute

echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2

goto fail

Expand All @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar

:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd

:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%

:mainEnd
if "%OS%"=="Windows_NT" endlocal
Expand Down
26 changes: 25 additions & 1 deletion src/main/java/run/halo/gradle/HaloDevtoolsPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@
import run.halo.gradle.extension.HaloExtension;
import run.halo.gradle.extension.HaloPluginExtension;
import run.halo.gradle.utils.YamlUtils;
import run.halo.gradle.openapi.ApiClientGeneratorTask;
import run.halo.gradle.openapi.CleanupApiServerContainer;
import run.halo.gradle.openapi.OpenApiDocsGeneratorTask;
import run.halo.gradle.watch.WatchTask;


/**
* @author guqing
* @since 2.0.0
Expand Down Expand Up @@ -172,9 +174,31 @@ public void apply(Project project) {
it.dependsOn("createHaloContainer");
});

project.getTasks().create(OpenApiDocsGeneratorTask.TASK_NAME,
OpenApiDocsGeneratorTask.class, it -> {
it.setGroup(GROUP);
it.setDescription("Generate open api docs.");
it.dependsOn("build", "pullHaloImage");
it.getImageId().set(imageName);
});

project.getTasks().create("generateApiClient", ApiClientGeneratorTask.class, it -> {
it.setGroup(GROUP);
it.setDescription("Generate api client code from open api spec.");
it.dependsOn("generateOpenApiDocs");
});

project.getTasks().withType(AbstractDockerRemoteApiTask.class)
.configureEach(task -> task.getDockerClientService().set(serviceProvider));

Provider<CleanupApiServerContainer> cleanupOpenApiDocServerContainer =
project.getGradle().getSharedServices()
.registerIfAbsent("cleanupOpenApiDocServerContainer",
CleanupApiServerContainer.class,
spec -> spec.parameters(
parameters -> parameters.getDockerClientService().set(serviceProvider))
);
buildEvents.onOperationCompletion(cleanupOpenApiDocServerContainer);

Provider<HaloServerBuildOperationListener> haloServerBuildOperationListenerProvider =
project.getGradle().getSharedServices()
Expand Down
13 changes: 12 additions & 1 deletion src/main/java/run/halo/gradle/docker/DockerCreateContainer.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package run.halo.gradle.docker;

import static run.halo.gradle.utils.HaloServerConfigure.buildPluginConfigYamlPath;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.github.dockerjava.api.command.CreateContainerCmd;
Expand Down Expand Up @@ -229,7 +231,7 @@ private void setContainerCommandConfig(CreateContainerCmd containerCommand) {
HostConfig hostConfig = new HostConfig();
hostConfig.withPortBindings(portBindings);

File projectDir = getProject().getBuildDir();
File projectDir = getProject().getLayout().getBuildDirectory().getAsFile().get();

List<Bind> binds = new ArrayList<>();
binds.add(new Bind(projectDir.toString(),
Expand All @@ -238,6 +240,15 @@ private void setContainerCommandConfig(CreateContainerCmd containerCommand) {
var sourceDir = pluginWorkplaceDir.getAsFile().get().getAbsolutePath();
binds.add(new Bind(sourceDir, new Volume(haloWorkDir())));
}

var pluginConfigYaml = pluginExtension.getConfigurationPropertiesFile()
.getAsFile().getOrNull();
if (pluginConfigYaml != null && Files.exists(pluginConfigYaml.toPath())) {
binds.add(new Bind(pluginConfigYaml.getAbsolutePath(),
new Volume(
buildPluginConfigYamlPath(haloExtension.getServerWorkDir(), pluginName))));
}

hostConfig.withBinds(binds);

containerCommand.withHostConfig(hostConfig);
Expand Down
24 changes: 24 additions & 0 deletions src/main/java/run/halo/gradle/extension/HaloPluginExtension.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
import org.gradle.api.NamedDomainObjectContainer;
import org.gradle.api.Project;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.Property;
import run.halo.gradle.openapi.OpenApiExtension;
import run.halo.gradle.watch.WatchTarget;

@Data
Expand All @@ -24,18 +26,40 @@ public class HaloPluginExtension {

private final Property<String> mainClass;

private OpenApiExtension openApi;

private File manifestFile;

private RegularFileProperty configurationPropertiesFile;

private NamedDomainObjectContainer<WatchTarget> watchDomains;

public HaloPluginExtension(Project project) {
this.watchDomains = project.container(WatchTarget.class);
this.mainClass = project.getObjects().property(String.class);
this.workDir = project.getObjects().directoryProperty();
this.configurationPropertiesFile = project.getObjects().fileProperty();
this.openApi = project.getObjects()
.newInstance(OpenApiExtension.class, project.getExtensions());

this.configurationPropertiesFile.map(file -> {
var yaml = file.getAsFile();
if (!yaml.exists()) {
return null;
}
var yamlPath = yaml.toPath();
if (yamlPath.endsWith(".yaml") || yamlPath.endsWith(".yml")) {
return yaml;
}
throw new IllegalArgumentException("Plugin configuration file must be a YAML file.");
});
this.workDir.convention(project.getLayout().getProjectDirectory().dir("workplace"));
}

public void openApi(Action<OpenApiExtension> action) {
action.execute(openApi);
}

public String getRequires() {
if (this.requires == null || "*".equals(requires)) {
return "latest";
Expand Down
45 changes: 45 additions & 0 deletions src/main/java/run/halo/gradle/openapi/ApiClientExtension.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package run.halo.gradle.openapi;

import java.util.HashMap;
import javax.inject.Inject;
import lombok.Data;
import lombok.ToString;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.ProjectLayout;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.provider.MapProperty;
import org.gradle.api.provider.Property;

@Data
@ToString
public class ApiClientExtension {
private DirectoryProperty outputDir;
private Property<String> generatorName;
private MapProperty<String, Object> additionalProperties;
private MapProperty<String, String> typeMappings;

@Inject
public ApiClientExtension(ObjectFactory objectFactory, ProjectLayout layout) {
this.outputDir = objectFactory.directoryProperty();
this.generatorName = objectFactory.property(String.class);
this.additionalProperties = objectFactory.mapProperty(String.class, Object.class);
this.typeMappings = objectFactory.mapProperty(String.class, String.class);

// init
outputDir.convention(layout.getProjectDirectory()
.dir("console/src/api/generated"));
this.generatorName.convention("typescript-axios");

var properties = new HashMap<String, Object>();
properties.put("useES6", true);
properties.put("useSingleRequestParameter", true);
properties.put("withSeparateModelsAndApi", true);
properties.put("apiPackage", "api");
properties.put("modelPackage", "models");
this.additionalProperties.convention(properties);

var typeMappings = new HashMap<String, String>();
typeMappings.put("set", "Array");
this.typeMappings.convention(typeMappings);
}
}
Loading

0 comments on commit 5f2e4be

Please sign in to comment.