diff --git a/api-generator/pom.xml b/api-spec-generator/pom.xml
similarity index 72%
rename from api-generator/pom.xml
rename to api-spec-generator/pom.xml
index 88a8f65..e632ed3 100644
--- a/api-generator/pom.xml
+++ b/api-spec-generator/pom.xml
@@ -9,7 +9,7 @@
1.0.0.0
- api-generator
+ api-spec-generator
@@ -27,13 +27,18 @@
3.1.0
- generate-js-client
- generate-sources
+ generate-spec
+ compile
java
- ru.kirillius.XCP.ApiGenerator.JavascriptClientGenerator
+ ru.kirillius.XCP.ApiGenerator.SpecGenerator
+
+ rpc/src/main/java
+ ru.kirillius.XCP.RPC.Services
+ target/generated-sources/api.spec.json
+
diff --git a/api-spec-generator/src/main/java/ru/kirillius/XCP/ApiGenerator/SpecGenerator.java b/api-spec-generator/src/main/java/ru/kirillius/XCP/ApiGenerator/SpecGenerator.java
new file mode 100644
index 0000000..07d6340
--- /dev/null
+++ b/api-spec-generator/src/main/java/ru/kirillius/XCP/ApiGenerator/SpecGenerator.java
@@ -0,0 +1,125 @@
+package ru.kirillius.XCP.ApiGenerator;
+
+import lombok.SneakyThrows;
+import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcMethod;
+import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcService;
+import tools.jackson.databind.JsonNode;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.node.ArrayNode;
+import tools.jackson.databind.node.JsonNodeFactory;
+import tools.jackson.databind.node.ObjectNode;
+
+import java.io.*;
+import java.util.Collection;
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+public class SpecGenerator {
+ /**
+ * Args:
+ * 1 - path to find classes
+ * 2 - search in package
+ * 3 - output spec file path
+ *
+ * @param args
+ * @throws IOException
+ */
+ public static void main(String[] args) throws IOException {
+ var generator = new SpecGenerator(
+ new File(args[0]),
+ args[1],
+ new File(args[2])
+ );
+
+ generator.generate();
+ generator.writeSpecs();
+ }
+
+ private final File scanDir;
+ private final String packageName;
+ private final File outputFile;
+
+ public SpecGenerator(File scanDir, String packageName, File outputFile) {
+ this.scanDir = scanDir;
+ this.packageName = packageName;
+ this.outputFile = outputFile;
+ }
+
+ private final ObjectNode specs = JsonNodeFactory.instance.objectNode();
+
+ @SneakyThrows
+ public void generate() {
+ specs.removeAll();
+ var scanPath = new File(scanDir, packageName.replaceAll(Pattern.quote("."), "/"));
+ for (var file : Objects.requireNonNull(scanPath.listFiles())) {
+ if (!file.getName().endsWith(".java") || file.getName().contains("$")) {
+ continue;
+ }
+ var className = packageName + '.' + file.getName();
+ className = className.substring(0, className.length() - ".java".length());
+
+ var cls = Class.forName(className);
+
+ if (!JsonRpcService.class.isAssignableFrom(cls)) {
+ continue;
+ }
+
+ var classSpecs = specs.putObject(cls.getSimpleName());
+ classSpecs.put("name", cls.getSimpleName());
+ var methods = classSpecs.putArray("methods");
+
+ for (var method : cls.getDeclaredMethods()) {
+ var descriptor = method.getAnnotation(JsonRpcMethod.class);
+ if (descriptor == null) {
+ continue;
+ }
+ var methodSpecs = methods.addObject();
+ methodSpecs.put("name", method.getName());
+ methodSpecs.put("description", descriptor.description());
+ methodSpecs.put("return", getTypeName(descriptor.returnType()));
+ methodSpecs.put("accessLevel", descriptor.accessLevel().name());
+
+ var params = methodSpecs.putArray("params");
+
+ for (var parameter : descriptor.parameters()) {
+ var paramSpecs = params.addObject();
+ paramSpecs.put("name", parameter.name());
+ paramSpecs.put("type", getTypeName(parameter.type()));
+ paramSpecs.put("description", parameter.description());
+ paramSpecs.put("optional", parameter.optional());
+ }
+
+ methodSpecs.put("return", getTypeName(descriptor.returnType()));
+
+ }
+
+ }
+
+ }
+
+ public void writeSpecs() throws IOException {
+ outputFile.getParentFile().mkdirs();
+ try (var stream = new FileOutputStream(outputFile)) {
+ var mapper = new ObjectMapper();
+ mapper.writerWithDefaultPrettyPrinter().writeValue(stream, specs);
+ }
+ }
+
+ private static String getTypeName(Class> type) {
+ if (type == null) return "unknown";
+ if (type == boolean.class) return "boolean";
+ if (type == int.class || type == long.class) return "number";
+ if (type == String.class) return "string";
+ if (type == void.class || type == Void.class) return "void";
+
+ if (type.isArray() || Collection.class.isAssignableFrom(type) || type == ArrayNode.class) {
+ return "Array";
+ }
+
+ if (JsonNode.class.isAssignableFrom(type)) {
+ return "Object";
+ }
+
+ return type.getSimpleName();
+ }
+}
diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/GroupRepositoryImpl.java b/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/GroupRepositoryImpl.java
index b87620f..77a9ca0 100644
--- a/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/GroupRepositoryImpl.java
+++ b/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/GroupRepositoryImpl.java
@@ -8,7 +8,7 @@ import org.hibernate.annotations.UuidGenerator;
import ru.kirillius.XCP.Commons.StreamHandler;
import ru.kirillius.XCP.Persistence.Entities.Group;
import ru.kirillius.XCP.Persistence.*;
-import ru.kirillius.XCP.Serialization.SerializationUtils;
+import tools.jackson.databind.node.JsonNodeFactory;
import tools.jackson.databind.node.ObjectNode;
import java.io.IOException;
@@ -157,7 +157,7 @@ public class GroupRepositoryImpl extends AbstractNodeRepository implement
@JsonProperty
@Getter
@Setter
- private ObjectNode properties = SerializationUtils.EmptyObject();
+ private ObjectNode properties = JsonNodeFactory.instance.objectNode();
@JsonIgnore
@ManyToMany(fetch = FetchType.EAGER)
diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/InputRepositoryImpl.java b/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/InputRepositoryImpl.java
index 2d77e49..cc6546d 100644
--- a/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/InputRepositoryImpl.java
+++ b/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/InputRepositoryImpl.java
@@ -11,9 +11,9 @@ import ru.kirillius.XCP.Persistence.Entities.Group;
import ru.kirillius.XCP.Persistence.Entities.Input;
import ru.kirillius.XCP.Persistence.*;
import ru.kirillius.XCP.Serialization.PollSettingsConverter;
-import ru.kirillius.XCP.Serialization.SerializationUtils;
import ru.kirillius.XCP.Serialization.ValueTransformationChainConverter;
import tools.jackson.databind.annotation.JsonDeserialize;
+import tools.jackson.databind.node.JsonNodeFactory;
import tools.jackson.databind.node.ObjectNode;
import java.util.HashSet;
@@ -110,7 +110,7 @@ public class InputRepositoryImpl extends AbstractNodeRepository implement
@JsonProperty
@Getter
@Setter
- private ObjectNode properties = SerializationUtils.EmptyObject();
+ private ObjectNode properties = JsonNodeFactory.instance.objectNode();
@JsonIgnore
@ManyToMany(fetch = FetchType.EAGER)
diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/OutputRepositoryImpl.java b/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/OutputRepositoryImpl.java
index aa85291..a73c67f 100644
--- a/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/OutputRepositoryImpl.java
+++ b/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/OutputRepositoryImpl.java
@@ -9,8 +9,8 @@ import ru.kirillius.XCP.Data.ValueTransformationChain;
import ru.kirillius.XCP.Persistence.Entities.Group;
import ru.kirillius.XCP.Persistence.Entities.Output;
import ru.kirillius.XCP.Persistence.*;
-import ru.kirillius.XCP.Serialization.SerializationUtils;
import ru.kirillius.XCP.Serialization.ValueTransformationChainConverter;
+import tools.jackson.databind.node.JsonNodeFactory;
import tools.jackson.databind.node.ObjectNode;
import java.util.HashSet;
@@ -106,7 +106,7 @@ public class OutputRepositoryImpl extends AbstractNodeRepository