diff --git a/Sources/CodeGenerator/Models/GenerationModels/OperationGenerationModel.swift b/Sources/CodeGenerator/Models/GenerationModels/OperationGenerationModel.swift index fc6ea8c8..56cd2209 100644 --- a/Sources/CodeGenerator/Models/GenerationModels/OperationGenerationModel.swift +++ b/Sources/CodeGenerator/Models/GenerationModels/OperationGenerationModel.swift @@ -1,6 +1,6 @@ // // OperationGenerationModel.swift -// +// // // Created by Александр Кравченков on 19.10.2021. // @@ -63,11 +63,13 @@ public struct OperationGenerationModel: Encodable { public let requestModel: Reference? public let pathParameters: [ParameterModel] + public let headerParameters: [ParameterModel] public let queryParameters: [ParameterModel] public let requestGenerationModel: DataGenerationModel? public let responseGenerationModel: Keyed? public let allGenerationResponses: [ResponseGenerationModel]? + public let id: String? init(operationModel: OperationModel) { self.httpMethod = operationModel.httpMethod @@ -82,6 +84,7 @@ public struct OperationGenerationModel: Encodable { .map { $0.value } .sorted { $0.name < $1.name } self.pathParameters = allParameters.filter { $0.location == .path } + self.headerParameters = allParameters.filter { $0.location == .header } self.queryParameters = allParameters.filter { $0.location == .query } let request = operationModel.requestModel?.value @@ -108,6 +111,7 @@ public struct OperationGenerationModel: Encodable { responses: response.values.map { DataGenerationModel(dataModel: $0) } ) } + self.id = operationModel.id } } diff --git a/Sources/CodeGenerator/Models/PropertyModel.swift b/Sources/CodeGenerator/Models/PropertyModel.swift index 42e8bc3f..0220a94d 100644 --- a/Sources/CodeGenerator/Models/PropertyModel.swift +++ b/Sources/CodeGenerator/Models/PropertyModel.swift @@ -79,6 +79,12 @@ public struct PropertyModel { public let description: String? public let type: PossibleType public let isNullable: Bool + public let example: Any? + public let format: String? + public let minimum: Double? + public let maximum: Double? + public let maxLength: Int? + public let minLength: Int? /// This value will be used as type for generation public let typeModel: ItemTypeModel @@ -86,7 +92,13 @@ public struct PropertyModel { init(name: String, description: String?, type: PropertyModel.PossibleType, - isNullable: Bool) { + isNullable: Bool, + example: Any?, + format: String?, + minimum: Double?, + maximum: Double?, + maxLength: Int?, + minLength: Int?) { self.name = name self.description = description self.type = type @@ -96,6 +108,12 @@ public struct PropertyModel { isObject: type.isObject, enumTypeName: type.enumTypeName, aliasTypeName: type.aliasTypeName) + self.example = example + self.format = format + self.minimum = minimum + self.maximum = maximum + self.minLength = minLength + self.maxLength = maxLength } } diff --git a/Sources/CodeGenerator/Models/ServiceMethod/OperationModel.swift b/Sources/CodeGenerator/Models/ServiceMethod/OperationModel.swift index 94fd48ac..381041b4 100644 --- a/Sources/CodeGenerator/Models/ServiceMethod/OperationModel.swift +++ b/Sources/CodeGenerator/Models/ServiceMethod/OperationModel.swift @@ -1,6 +1,6 @@ // // OperationModel.swift -// +// // // Created by Александр Кравченков on 17.12.2020. // @@ -56,19 +56,21 @@ public struct OperationModel: Encodable { public let parameters: [Reference]? public let responses: [Reference]? public let requestModel: Reference? + public let id: String? init(httpMethod: String, summary: String?, description: String?, parameters: [Reference]?, responses: [Reference]?, - requestModel: Reference?) { + requestModel: Reference?, + id: String?) { self.httpMethod = httpMethod self.summary = summary - self.description = description self.parameters = parameters self.responses = responses self.requestModel = requestModel + self.id = id } } diff --git a/Sources/CodeGenerator/Stages/TreeParserStage/TreeParser/Resolver.swift b/Sources/CodeGenerator/Stages/TreeParserStage/TreeParser/Resolver.swift index f39163e4..ba0f941f 100644 --- a/Sources/CodeGenerator/Stages/TreeParserStage/TreeParser/Resolver.swift +++ b/Sources/CodeGenerator/Stages/TreeParserStage/TreeParser/Resolver.swift @@ -190,12 +190,24 @@ public class Resolver { return .init(name: property.name, description: property.description, type: .array(.init(name: "", itemsType: try arrayItemTypeUnwrapper(arr.itemsType))), - isNullable: property.nullable) + isNullable: property.nullable, + example: property.example, + format: property.format, + minimum: property.minimum, + maximum: property.maximum, + maxLength: property.maxLength, + minLength: property.minLength) case .simple(let val): return .init(name: property.name, description: property.description, type: try propertyTypeUnwrapper(val), - isNullable: property.nullable) + isNullable: property.nullable, + example: property.example, + format: property.format, + minimum: property.minimum, + maximum: property.maximum, + maxLength: property.maxLength, + minLength: property.minLength) } } diff --git a/Sources/CodeGenerator/Stages/TreeParserStage/TreeParser/TreeParser.swift b/Sources/CodeGenerator/Stages/TreeParserStage/TreeParser/TreeParser.swift index 331fbf1b..01f774aa 100644 --- a/Sources/CodeGenerator/Stages/TreeParserStage/TreeParser/TreeParser.swift +++ b/Sources/CodeGenerator/Stages/TreeParserStage/TreeParser/TreeParser.swift @@ -1,6 +1,6 @@ // // File.swift -// +// // // Created by Александр Кравченков on 17.12.2020. // @@ -67,7 +67,7 @@ public struct TreeParser { func parse(operation: OperationNode, current: DependencyWithTree, other: [DependencyWithTree]) throws -> OperationModel { let params = try operation.parameters.map { parameter -> Reference in - + return try wrap( self.parametersParser.parse(parameter: parameter, current: current, other: other), message: "While parsing parameter \(parameter.view)") @@ -94,7 +94,8 @@ public struct TreeParser { description: operation.description, parameters: params, responses: responses, - requestModel: requestBody + requestModel: requestBody, + id: operation.id ) } } diff --git a/Sources/GASTBuilder/Builders/AnySchemaBuilder.swift b/Sources/GASTBuilder/Builders/AnySchemaBuilder.swift index a3badf7c..24c8d8a9 100644 --- a/Sources/GASTBuilder/Builders/AnySchemaBuilder.swift +++ b/Sources/GASTBuilder/Builders/AnySchemaBuilder.swift @@ -161,25 +161,63 @@ public struct AnySchemaBuilder: SchemaBuilder { func build(object: ObjectSchema, meta: Metadata, name: String, apiDefinitionFileRef: String) throws -> SchemaModelNode { let properties = try object.properties.map { property -> PropertyNode in - let type = try wrap(property.schema.extractType(), - message: "In object \(name), in property \(property.name)") - + let schema = property.schema + let type = try wrap( + schema.extractType(), + message: "In object \(name), in property \(property.name)" + ) var isNullable = property.isNullable - if self.useNewNullableDeterminationStrategy { - isNullable = property.schema.metadata.nullable + isNullable = schema.metadata.nullable } + var format: String? + var minimum: Double? + var maximum: Double? + var minLength: Int? + var maxLength: Int? - return PropertyNode(name: property.name, - type: type, - description: property.schema.metadata.description, - example: property.schema.metadata.example, - nullable: isNullable) + switch schema.type { + case .string(let stringSchema): + format = stringSchema.format?.rawValue + minLength = stringSchema.minLength + maxLength = stringSchema.maxLength + + case .number(let numberSchema): + format = numberSchema.format?.rawValue + minimum = numberSchema.minimum + maximum = numberSchema.maximum + + case .integer(let integerSchema): + format = integerSchema.format?.rawValue + minimum = integerSchema.minimum.flatMap(Double.init) + maximum = integerSchema.maximum.flatMap(Double.init) + + default: + break + } + + return PropertyNode( + name: property.name, + type: type, + description: schema.metadata.description, + example: schema.metadata.example, + nullable: isNullable, + format: format, + minimum: minimum, + maximum: maximum, + maxLength: maxLength, + minLength: minLength + ) } - return SchemaModelNode(name: name, properties: properties, description: meta.description, apiDefinitionFileRef: apiDefinitionFileRef) + return SchemaModelNode( + name: name, + properties: properties, + description: meta.description, + apiDefinitionFileRef: apiDefinitionFileRef + ) } func build(array: ArraySchema, name: String, apiDefinitionFileRef: String) throws -> SchemaArrayNode { diff --git a/Sources/GASTBuilder/Builders/AnyServiceBuilder.swift b/Sources/GASTBuilder/Builders/AnyServiceBuilder.swift index 375a8ae8..b4105d2a 100644 --- a/Sources/GASTBuilder/Builders/AnyServiceBuilder.swift +++ b/Sources/GASTBuilder/Builders/AnyServiceBuilder.swift @@ -1,6 +1,6 @@ // // AnyServiceBuilder.swift -// +// // // Created by Александр Кравченков on 14.12.2020. // @@ -17,7 +17,7 @@ public protocol ServiceBuilder { } /// Default implementation for `ServiceBuilder` -/// +/// /// Builds `path` elements of Open-API spec /// /// **WARNING** @@ -34,7 +34,6 @@ public struct AnyServiceBuilder: ServiceBuilder { let schemaBuilder: SchemaBuilder let requestBodyBuilder: RequestBodyBuilder let responseBuilder: ResponseBuilder - public init(parameterBuilder: ParametersBuilder, schemaBuilder: SchemaBuilder, requestBodyBuilder: RequestBodyBuilder, @@ -93,7 +92,8 @@ extension AnyServiceBuilder { summary: operation.summary, parameters: params, requestBody: requestBody, - responses: responses) + responses: responses, + id: operation.generatedIdentifier) } } diff --git a/Sources/GASTTree/PropertyNode.swift b/Sources/GASTTree/PropertyNode.swift index 2ec84c1b..0f92caa9 100644 --- a/Sources/GASTTree/PropertyNode.swift +++ b/Sources/GASTTree/PropertyNode.swift @@ -19,17 +19,32 @@ public struct PropertyNode { public let description: String? public let example: Any? public let nullable: Bool + public let format: String? + public let minimum: Double? + public let maximum: Double? + public let maxLength: Int? + public let minLength: Int? public init(name: String, type: PossibleType, description: String?, example: Any?, - nullable: Bool) { + nullable: Bool, + format: String?, + minimum: Double?, + maximum: Double?, + maxLength: Int?, + minLength: Int?) { self.name = name self.type = type self.description = description self.example = example self.nullable = nullable + self.format = format + self.minimum = minimum + self.maximum = maximum + self.minLength = minLength + self.maxLength = maxLength } } diff --git a/Sources/GASTTree/Services/OperationNode.swift b/Sources/GASTTree/Services/OperationNode.swift index c5713fdd..ca0b5299 100644 --- a/Sources/GASTTree/Services/OperationNode.swift +++ b/Sources/GASTTree/Services/OperationNode.swift @@ -1,6 +1,6 @@ // // OperationNode.swift -// +// // // Created by Александр Кравченков on 14.12.2020. // @@ -27,18 +27,21 @@ public struct OperationNode { public let parameters: [Referenced] public let requestBody: Referenced? public let responses: [ResponseBody] + public let id: String? public init(method: String, description: String?, summary: String?, parameters: [Referenced], requestBody: Referenced?, - responses: [ResponseBody]) { + responses: [ResponseBody], + id: String?) { self.method = method self.description = description self.summary = summary self.parameters = parameters self.requestBody = requestBody self.responses = responses + self.id = id } } diff --git a/Sources/SurfGenKit/Models/GenerationModels/OperationGenerationModel.swift b/Sources/SurfGenKit/Models/GenerationModels/OperationGenerationModel.swift index 92841f42..c3335391 100644 --- a/Sources/SurfGenKit/Models/GenerationModels/OperationGenerationModel.swift +++ b/Sources/SurfGenKit/Models/GenerationModels/OperationGenerationModel.swift @@ -1,6 +1,6 @@ // // OperationGenerationModel.swift -// +// // // Created by Dmitry Demyanov on 04.11.2020. // @@ -11,7 +11,7 @@ enum HttpMethod: String { case patch case put case delete - + var name: String { switch self { case .get, .post, .delete: @@ -29,7 +29,7 @@ enum ResponseBody: Equatable { } public struct OperationGenerationModel { - + private enum Constants { static let multipartModel = "MultipartModel" } @@ -46,11 +46,12 @@ public struct OperationGenerationModel { let hasBody: Bool var requestBody: RequestBodyGenerationModel? + let id: String? private(set) var hasUndefinedResponseBody = false private(set) var hasResponseModel = false private(set) var responseModel: String? - + init(name: String, description: String?, path: PathGenerationModel, @@ -58,7 +59,8 @@ public struct OperationGenerationModel { pathParameters: [ParameterGenerationModel], queryParameters: [ParameterGenerationModel], requestBody: RequestBodyGenerationModel.BodyType?, - responseBody: ResponseBody) { + responseBody: ResponseBody, + id: String?) { self.name = name self.hasDescription = description != nil self.description = description @@ -70,7 +72,7 @@ public struct OperationGenerationModel { self.queryParameters = queryParameters self.hasBody = requestBody != nil self.requestBody = RequestBodyGenerationModel(type: requestBody) - + self.id = id switch responseBody { case .model(let modelName): self.hasResponseModel = true diff --git a/Sources/SurfGenKit/Parsers/OperationNodeParser.swift b/Sources/SurfGenKit/Parsers/OperationNodeParser.swift index 68746a0d..e20c33fd 100644 --- a/Sources/SurfGenKit/Parsers/OperationNodeParser.swift +++ b/Sources/SurfGenKit/Parsers/OperationNodeParser.swift @@ -1,6 +1,6 @@ // // OperationNodeParser.swift -// +// // // Created by Dmitry Demyanov on 07.11.2020. // @@ -17,7 +17,7 @@ class OperationNodeParser { private let mediaContentParser: MediaContentNodeParser private let parametersParser: ParametersNodeParser - + public var logger: Loger = DefaultLogger.default private let platform: Platform init(mediaContentParser: MediaContentNodeParser, parametersParser: ParametersNodeParser, platform: Platform) { @@ -80,16 +80,17 @@ class OperationNodeParser { let responseBody = try wrap(mediaContentParser.parseResponseBody(node: operation.subNodes.responseBodyNode, forOperationName: name), with: ErrorMessages.errorMessage(for: name)) - + return OperationGenerationModel(name: name, description: description, path: pathModel, httpMethod: method, - pathParameters: parameters.filter { $0.location == .path }, + pathParameters: parameters.filter { $0.location == .path || $0.location == .header}, queryParameters: parameters.filter { $0.location == .query }, requestBody: requestBody, - responseBody: responseBody) + responseBody: responseBody, + id: name) } - + } diff --git a/Templates/v2/Java/api.stencil b/Templates/v2/Java/api.stencil new file mode 100644 index 00000000..527bdf98 --- /dev/null +++ b/Templates/v2/Java/api.stencil @@ -0,0 +1,25 @@ +package ru.surf.surfgen.api.contract; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "{{ service.name }} API") +public interface {{ service.name }}Api { + {%for path in service.paths%} + {%for operation in path.operations%} + @Operation(summary = "{{ operation.summary }}") + {%if operation.httpMethod|lowercase == "delete"%} + @ResponseStatus(HttpStatus.NO_CONTENT) + {%endif%} + @{{ operation.httpMethod|capitalizeFirstLetter }}Mapping("{{ path.path }}") + {%if operation.httpMethod|lowercase == "delete"%}void{%else%}ResponseEntity<{{ operation.responseGenerationModel.value.typeNames|join }}>{%endif%} {{operation.id}}({%for parameter in operation.pathParameters%} + @PathVariable("{{ parameter.name }}") {{ parameter.typeModel.name|capitalizeFirstLetter }} {{ parameter.name|snakeCaseToCamelCase }}{%endfor%} + {%for parameter in operation.headerParameters%} + @RequestHeader(name = "{{ parameter.name }}", required={{ parameter.isRequired }}) String {{ parameter.schema.type|capitalizeFirstLetter }} {{ parameter.name|snakeCaseToCamelCase }}{%endfor%} + {%for parameter in operation.queryParameters%}@RequestParam(name = "{{ parameter.name }}", required={{ parameter.isRequired }}) {{ parameter.typeModel.name|capitalizeFirstLetter }} {{ parameter.name|snakeCaseToCamelCase }}{%endfor%} + {%for requestModelName in operation.requestGenerationModel.typeNames%}@Valid @RequestBody {{ requestModelName|capitalizeFirstLetter }} {{ requestModelName|snakeCaseToCamelCase }}{%endfor%}); + {%endfor%} + {%endfor%} +} \ No newline at end of file diff --git a/Templates/v2/Java/config/java.config.yaml b/Templates/v2/Java/config/java.config.yaml new file mode 100644 index 00000000..78afcb1b --- /dev/null +++ b/Templates/v2/Java/config/java.config.yaml @@ -0,0 +1,31 @@ +useNewNullableDeterminationStrategy: false + +prefixesToCutDownInServiceNames: + - /api/v1.1 + - /api/1.1 + +analytcsConfig: + logstashEnpointURI: http://logs.ps.surfstudio.ru + payload: + project: TEST + +templates: + - type: service + nameSuffix: Api + fileExtension: java + templatePath: + destinationPath: + - type: service + nameSuffix: Controller + fileExtension: java + templatePath: + destinationPath: + - type: model + nameSuffix: + fileExtension: java + templatePath: + destinationPath: + - type: enum + fileExtension: java + templatePath: + destinationPath: \ No newline at end of file diff --git a/Templates/v2/Java/controller.stencil b/Templates/v2/Java/controller.stencil new file mode 100644 index 00000000..cf997484 --- /dev/null +++ b/Templates/v2/Java/controller.stencil @@ -0,0 +1,45 @@ +package ru.surf.surfgen.controller; + +import ru.surf.surfgen.api.contract.{{ service.name }}Api; +import ru.surf.surfgen.service.{{ service.name }}Service; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +public class {{ service.name }}Controller implements {{ service.name }}Api { + private final {{ service.name }}Service {{ service.name|lowercaseFirstLetter }}Service; + + {%for path in service.paths%} + {%for operation in path.operations%} + @Override + public {%if operation.responseGenerationModel.key == "204"%}void{%else%}ResponseEntity<{{ operation.responseGenerationModel.value.typeNames|join }}>{%endif%} {{ operation.id }}( + {%for parameter in operation.pathParameters%}{{ parameter.typeModel.name|capitalizeFirstLetter }} {{ parameter.name|snakeCaseToCamelCase }}{%endfor%} + {%for parameter in operation.headerParameters%}{{ parameter.typeModel.name|capitalizeFirstLetter }} {{ parameter.name|snakeCaseToCamelCase }}{%endfor%} + {%for parameter in operation.queryParameters%}{{ parameter.typeModel.name|capitalizeFirstLetter }} {{ parameter.name|snakeCaseToCamelCase }}{%endfor%} + {%for requestModelName in operation.requestGenerationModel.typeNames%} {{ requestModelName|capitalizeFirstLetter }} {{ requestModelName|snakeCaseToCamelCase }}{%endfor%}) { + {%if operation.responseGenerationModel.key == "204"%} + {{ service.name|lowercaseFirstLetter }}Service.{{ operation.id }}( + {%for parameter in operation.pathParameters%}{{ parameter.name|snakeCaseToCamelCase }}{%endfor%} + {%for parameter in operation.headerParameters%}{{ parameter.typeModel.name|capitalizeFirstLetter }} {{ parameter.name|snakeCaseToCamelCase }}{%endfor%} + {%for parameter in operation.queryParameters%}{{ parameter.name|snakeCaseToCamelCase }}{%endfor%} + {%for requestModelName in operation.requestGenerationModel.typeNames%}{{ requestModelName|snakeCaseToCamelCase }}{%endfor%}); + {%else%} + return {%if operation.responseGenerationModel.key == "201"%}new ResponseEntity<>({{ service.name|lowercaseFirstLetter }}Service.{{ operation.id }}( + {%for parameter in operation.pathParameters%}{{ parameter.name|snakeCaseToCamelCase }}{%endfor%} + {%for parameter in operation.queryParameters%}{{ parameter.name|snakeCaseToCamelCase }}{%endfor%} + {%for parameter in operation.headerParameters%}{{ parameter.name|snakeCaseToCamelCase }}{%endfor%} + {%for requestModelName in operation.requestGenerationModel.typeNames%}{{ requestModelName|snakeCaseToCamelCase }}{%endfor%}), HttpStatus.CREATED); + {%else%} + ResponseEntity.ok({{ service.name|lowercaseFirstLetter }}Service.{{ operation.id }}( + {%for parameter in operation.pathParameters%}{{ parameter.name|snakeCaseToCamelCase }}{%endfor%} + {%for parameter in operation.queryParameters%}{{ parameter.name|snakeCaseToCamelCase }}{%endfor%} + {%for parameter in operation.headerParameters%}{{ parameter.name|snakeCaseToCamelCase }}{%endfor%} + {%for requestModelName in operation.requestGenerationModel.typeNames%}{{ requestModelName|snakeCaseToCamelCase }}{%endfor%})); + {%endif%} + {%endif%} + } + {%endfor%} + {%endfor%} +} \ No newline at end of file diff --git a/Templates/v2/Java/dto.stencil b/Templates/v2/Java/dto.stencil new file mode 100644 index 00000000..fd3f4ce5 --- /dev/null +++ b/Templates/v2/Java/dto.stencil @@ -0,0 +1,24 @@ +package ru.surf.surfgen.api.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.NoArgsConstructor; +import javax.validation.constraints.*; +import java.util.List; +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +public class {{ model.name }} { + {% for property in model.properties %} + @Schema( description = "{{ property.description }}"{% if property.example %}, example = "{{ property.example }}"{% else %}){% endif %}{% if not property.isNullable %} + @NotNull{% if property.typeModel.name == "string" %} + @NotBlank{% endif %}{% elif property.isNullable %} + @Nullable{% endif %}{% if property.typeModel.name == "string" %}{% if property.minLength or property.maxLength %} + @Size(min = {{ property.minLength }}, max = {{ property.maxLength }}){% endif %}{% elif property.typeModel.name == "integer" %}{% if property.minimum or property.maximum %} + @Min({{ property.minimum}}) + @Max({{ property.maximum }}){% endif %}{% endif %} + private {% if property.typeModel.isArray %}List<{{ property.typeModel.name|capitalizeFirstLetter }}>{% else %}{% if property.typeModel.name == "integer" %}{% if property.format == "int64" %}Long{% else %}Integer{% endif %}{% elif property.typeModel.name == "number" %}Double{% elif property.typeModel.name == "string" %}{% if property.format == "date-time" %}LocalDateTime{% elif property.format == "date" %}LocalDate{% else %}String{% endif %}{% else %}{{ property.typeModel.name|capitalizeFirstLetter }}{% endif %}{% endif %} {{ property.name|snakeCaseToCamelCase }}; + {% endfor %} +} \ No newline at end of file diff --git a/Templates/v2/Java/enum.stencil b/Templates/v2/Java/enum.stencil new file mode 100644 index 00000000..4ffdb749 --- /dev/null +++ b/Templates/v2/Java/enum.stencil @@ -0,0 +1,30 @@ +{% if enum.description %}/** + {% for line in enum.description|splitLines %}* {{ line }} + {% endfor %}*/ +{% endif %}public enum {{ enum.name }} { + {% for caseValue in enum.cases %}{# + #}{{ caseValue|uppercase }}("{{ caseValue }}"){% if not forloop.last %},{% else %};{% endif %} + {% endfor %} + + private final String value; + + {{ enum.name }}(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static {{ enum.name }} getBy(String value) { + if (value == null) { + return null; + } + for ({{ enum.name }} item : values()) { + if (item.value.equals(value)) { + return item; + } + } + return null; + } +} \ No newline at end of file