diff --git a/.github/agents/ctp-validator-rule-creator.agent.md b/.github/agents/ctp-validator-rule-creator.agent.md new file mode 100644 index 00000000..24cfd52f --- /dev/null +++ b/.github/agents/ctp-validator-rule-creator.agent.md @@ -0,0 +1,224 @@ +--- +name: ctp-validator-rule-creator +description: This agent creates new validation rules for the ctp-validators Kotlin project, including rule implementation, test cases, and test RAML files. +tools: ['read', 'edit', 'search', 'web'] +--- +# CTP Validator Rule Creator Agent + +You are a specialized agent for creating new validation rules in the ctp-validators Kotlin project. + +## Your Expertise + +You excel at: +1. Creating new validation rule Kotlin classes following the established patterns +2. Writing comprehensive test cases in Groovy +3. Creating test RAML files with both valid and invalid examples +4. Understanding the validation framework architecture + +## Project Structure + +- **Rule files location**: `ctp-validators/src/main/kotlin/com/commercetools/rmf/validators/` +- **Test file location**: `ctp-validators/src/test/groovy/com/commercetools/rmf/validators/ValidatorRulesTest.groovy` +- **Test RAML files**: `ctp-validators/src/test/resources/` + +## Rule Implementation Pattern + +### 1. Rule File Structure (Kotlin) + +Every rule file must follow this pattern: + +```kotlin +package com.commercetools.rmf.validators + +import io.vrap.rmf.raml.model.types.* +import org.eclipse.emf.common.util.Diagnostic +import java.util.* + +@ValidatorSet +class YourRuleNameRule(severity: RuleSeverity, options: List? = null) : TypesRule(severity, options) { + + // Optional: excludes for properties that should be exempt from the rule + private val exclude: List = + (options?.filter { ruleOption -> ruleOption.type.lowercase(Locale.getDefault()) == RuleOptionType.EXCLUDE.toString() } + ?.map { ruleOption -> ruleOption.value }?.plus("") ?: defaultExcludes) + + // Override the appropriate case method based on what you're validating + // Common options: caseObjectType, caseProperty, caseStringType, etc. + override fun caseObjectType(type: ObjectType): List { + val validationResults: MutableList = ArrayList() + + // Your validation logic here + // Use: error(object, "message", args...) or create(object, "message", args...) + + return validationResults + } + + companion object : ValidatorFactory { + private val defaultExcludes by lazy { listOf("") } + + @JvmStatic + override fun create(options: List?): YourRuleNameRule { + return YourRuleNameRule(RuleSeverity.ERROR, options) + } + + @JvmStatic + override fun create(severity: RuleSeverity, options: List?): YourRuleNameRule { + return YourRuleNameRule(severity, options) + } + } +} +``` + +**Key Points:** +- Annotate with `@ValidatorSet` +- Extend `TypesRule(severity, options)` +- Implement companion object with `ValidatorFactory` +- Use `error()` or `create()` methods to generate diagnostics +- Support exclusion options if needed +- File name must be PascalCase ending with `Rule.kt` (e.g., `BooleanPropertyNameRule.kt`) + +### 2. Test Case Structure (Groovy) + +Add a test method to `ValidatorRulesTest.groovy`: + +```groovy +def "your rule name test"() { + when: + def validators = Arrays.asList(new TypesValidator(Arrays.asList(YourRuleNameRule.create(emptyList())))) + def uri = uriFromClasspath("/your-rule-name.raml") + def result = new RamlModelBuilder(validators).buildApi(uri) + then: + result.validationResults.size == X + result.validationResults[0].message == "Expected error message" + result.validationResults[1].message == "Another expected error message" +} +``` + +**With exclusions:** +```groovy +def "your rule name test with exclusions"() { + when: + def options = singletonList(new RuleOption(RuleOptionType.EXCLUDE.toString(), "TypeName:propertyName")) + def validators = Arrays.asList(new TypesValidator(Arrays.asList(YourRuleNameRule.create(options)))) + def uri = uriFromClasspath("/your-rule-name.raml") + def result = new RamlModelBuilder(validators).buildApi(uri) + then: + result.validationResults.size == X +} +``` + +### 3. Test RAML File Structure + +Create a comprehensive test RAML file at `ctp-validators/src/test/resources/your-rule-name.raml`: + +```raml +#%RAML 1.0 +title: your rule name + +annotationTypes: + package: string + sdkBaseUri: string + +baseUri: https://api.europe-west1.commercetools.com + +types: + InvalidExample: + (package): Common + type: object + properties: + # Properties that violate the rule with clear comments + badProperty: + description: xyz + type: string + + ValidExample: + (package): Common + type: object + properties: + # Properties that follow the rule with clear comments + goodProperty: + description: xyz + type: string +``` + +**Key Points:** +- File name must be kebab-case matching the test: `your-rule-name.raml` +- Include both invalid and valid examples +- Add descriptive comments explaining why each example is valid/invalid +- Use clear type names like `InvalidXxx` and `ValidXxx` +- Test edge cases and boundary conditions + +## Validation Framework Components + +### Available Case Methods (from TypesSwitch) +- `caseObjectType(type: ObjectType)` - for validating object types +- `caseProperty(property: Property)` - for validating properties +- `caseStringType(type: StringType)` - for string types +- `caseBooleanType(type: BooleanType)` - for boolean types +- `caseArrayType(type: ArrayType)` - for array types +- And many more from the EMF TypesSwitch hierarchy + +### Helper Methods +- `error(object, message, args...)` - creates an ERROR diagnostic +- `create(object, message, args...)` - creates a diagnostic with the rule's severity +- Type checking helpers can be created as private extension methods + +### Common Patterns + +**Filtering with exclusions:** +```kotlin +if (exclude.contains(property.name).not()) { + // validation logic +} +``` + +**Pattern matching:** +```kotlin +if (property.name.matches(Regex("^is[A-Z].*$"))) { + validationResults.add(error(type, "Error message", property.name)) +} +``` + +**Type checking:** +```kotlin +private fun AnyType.isBoolean(): Boolean { + return when(this) { + is BooleanType -> true + else -> false + } +} +``` + +## Workflow for Creating a New Rule + +1. **Understand the requirement**: Clarify what the rule should validate +2. **Create test RAML examples**: Add to `ctp-validators/src/test/resources/your-rule-name.raml` with both valid and invalid test cases +3. **Create the rule file**: `YourRuleNameRule.kt` in the validators directory with appropriate `case` methods +4. **Add the test case**: Add test method to `ValidatorRulesTest.groovy` +5. **Run tests**: Use `./gradlew :ctp-validators:test` to verify +6. **Document externally**: Open a PR in the [commercetools-docs repository](https://github.com/commercetools/commercetools-docs) to add a description of the rule to the [validator rules page](https://github.com/commercetools/commercetools-docs/tree/main/api-specs#validator-rules) + +## Example Reference + +See `BooleanPropertyNameRule.kt` for a complete example that: +- Validates boolean property names don't have "is" prefix +- Supports exclusions +- Has comprehensive test coverage +- Includes clear error messages + +## Important Notes + +- All rule files must have the `@ValidatorSet` annotation +- Rule classes must extend `TypesRule` or another appropriate base validator +- Companion objects must implement `ValidatorFactory` +- Test RAML files should be comprehensive with both positive and negative cases +- Error messages should be descriptive and include context (property name, type name, etc.) +- Follow Kotlin coding conventions and existing code style +- Use the existing infrastructure (RuleOption, RuleSeverity, etc.) + +When creating a new rule, you should: +1. Ask clarifying questions about the validation requirements +2. Create the rule class with proper structure +3. Create comprehensive test RAML with edge cases +4. Add the test case to ValidatorRulesTest.groovy +5. Verify all three files work together correctly diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7131f69d..fcb9f228 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,5 @@ on: + workflow_dispatch: push: tags: - '[0-9]+.[0-9]+.[0-9]+*' @@ -9,14 +10,62 @@ permissions: {} jobs: release_maven: + permissions: + contents: write + actions: write + id-token: write + + name: Build and release + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '11' + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "18" + + - run: scripts/setup-signing-key.sh + env: + DECRYPTER: ${{ secrets.DECRYPTER }} + SIGNING_KEY: ${{ secrets.SIGNING_KEY }} + PASSPHRASE: ${{ secrets.PASSPHRASE }} + GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }} + GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }} + - name: Build and Release + run: export VERSION="1.0.0-`date '+%Y%m%d%H%M%S'`"; ./gradlew -Pversion=$VERSION clean check publishMavenPublicationToSonatype closeAndReleaseSonatypeStagingRepository writeVersionToReadme + env: + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - uses: stefanzweifel/git-auto-commit-action@v4.6.0 + with: + file_pattern: "scripts/install.sh" + commit_message: "TASK: Updating version in README" + commit_user_name: Auto Mation + commit_user_email: automation@commercetools.com + commit_author: Auto Mation + + release_tag: permissions: id-token: write contents: read name: Build and release to Maven + if: startsWith( github.ref, 'refs/tags/') runs-on: ubuntu-latest - + needs: [release_maven] steps: - name: Checkout uses: actions/checkout@v4 @@ -42,33 +91,39 @@ jobs: SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Setup Node + # Configure npm for OIDC authentication with trusted publishing + # This must be done after CI setup to ensure npm is properly configured + + # setup-node@v4 with registry-url automatically configures OIDC when id-token: write is set + - name: Setup Node.js for npm publishing uses: actions/setup-node@v4 with: - node-version: "18" + node-version: "24" + registry-url: "https://registry.npmjs.org" - name: Build NPM package run: | ./gradlew -Pversion=${{ github.ref_name }} tools:cli-application:shadowJar cp rmf-codegen.jar node/rmf-codegen/bin - - name: Creating .npmrc + - name: Verify npm OIDC configuration run: | - cat << EOF > "$HOME/.npmrc" - email=npmjs@commercetools.com - //registry.npmjs.org/:_authToken=$NPM_TOKEN - EOF - env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + # Verify registry is set correctly + echo "Registry: $(npm config get registry)" + # Ensure no token-based auth is configured (OIDC should be used automatically) + npm config delete //registry.npmjs.org/:_authToken || true + # Verify npm can access the registry (this will use OIDC if configured) + echo "npm OIDC authentication configured via setup-node action" - name: Publish npm package working-directory: node/rmf-codegen - run: yarn publish --no-git-tag-version --minor + run: npm version minor && npm publish --no-git-tag-version bump_version: + if: startsWith( github.ref, 'refs/tags/') name: Bump NPM version - needs: [release_maven] + needs: [release_tag] runs-on: ubuntu-latest permissions: diff --git a/.github/workflows/release_maven.yml b/.github/workflows/release_maven.yml deleted file mode 100644 index 602a5ed0..00000000 --- a/.github/workflows/release_maven.yml +++ /dev/null @@ -1,54 +0,0 @@ -on: - - workflow_dispatch - -name: Release to Maven Central - -permissions: {} - -jobs: - release: - permissions: - contents: write - actions: write - id-token: write - - name: Build and release - - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '11' - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: "18" - - - run: scripts/setup-signing-key.sh - env: - DECRYPTER: ${{ secrets.DECRYPTER }} - SIGNING_KEY: ${{ secrets.SIGNING_KEY }} - PASSPHRASE: ${{ secrets.PASSPHRASE }} - GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }} - GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }} - - name: Build and Release - run: export VERSION="1.0.0-`date '+%Y%m%d%H%M%S'`"; ./gradlew -Pversion=$VERSION clean check publishMavenPublicationToSonatype closeAndReleaseSonatypeStagingRepository writeVersionToReadme - env: - SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} - SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - uses: stefanzweifel/git-auto-commit-action@v4.6.0 - with: - file_pattern: "scripts/install.sh" - commit_message: "TASK: Updating version in README" - commit_user_name: Auto Mation - commit_user_email: automation@commercetools.com - commit_author: Auto Mation diff --git a/ctp-validators/src/main/kotlin/com/commercetools/rmf/validators/EnumValuePascalCaseRule.kt b/ctp-validators/src/main/kotlin/com/commercetools/rmf/validators/EnumValuePascalCaseRule.kt new file mode 100644 index 00000000..61920b6e --- /dev/null +++ b/ctp-validators/src/main/kotlin/com/commercetools/rmf/validators/EnumValuePascalCaseRule.kt @@ -0,0 +1,54 @@ +package com.commercetools.rmf.validators + +import io.vrap.rmf.raml.model.types.StringType +import io.vrap.rmf.raml.model.util.StringCaseFormat +import org.eclipse.emf.common.util.Diagnostic +import java.util.* + +@ValidatorSet +class EnumValuePascalCaseRule(severity: RuleSeverity, options: List? = null) : TypesRule(severity, options) { + + private val exclude: List = + (options?.filter { ruleOption -> ruleOption.type.lowercase(Locale.getDefault()) == RuleOptionType.EXCLUDE.toString() } + ?.map { ruleOption -> ruleOption.value }?.plus("") ?: defaultExcludes) + + override fun caseStringType(type: StringType): List { + val validationResults: MutableList = ArrayList() + + if (type.name != "string" && type.enum.isNullOrEmpty().not()) { + type.enum.forEach { enumValue -> + val enumName = enumValue.value as? String + if (enumName != null && exclude.contains("${type.name}:${enumName}").not() && exclude.contains("${type.name}").not() && !isPascalCase(enumName)) { + validationResults.add( + error( + type, + "Enum value \"{0}\" in type \"{1}\" must be PascalCase", + enumName, + type.name + ) + ) + } + } + } + + return validationResults + } + + private fun isPascalCase(value: String): Boolean { + return value == StringCaseFormat.UPPER_CAMEL_CASE.apply(value) + } + + companion object : ValidatorFactory { + private val defaultExcludes by lazy { listOf("") } + + @JvmStatic + override fun create(options: List?): EnumValuePascalCaseRule { + return EnumValuePascalCaseRule(RuleSeverity.ERROR, options) + } + + @JvmStatic + override fun create(severity: RuleSeverity, options: List?): EnumValuePascalCaseRule { + return EnumValuePascalCaseRule(severity, options) + } + } +} diff --git a/ctp-validators/src/main/kotlin/com/commercetools/rmf/validators/ErrorResponseStructureRule.kt b/ctp-validators/src/main/kotlin/com/commercetools/rmf/validators/ErrorResponseStructureRule.kt new file mode 100644 index 00000000..b24e4102 --- /dev/null +++ b/ctp-validators/src/main/kotlin/com/commercetools/rmf/validators/ErrorResponseStructureRule.kt @@ -0,0 +1,74 @@ +package com.commercetools.rmf.validators + +import io.vrap.rmf.raml.model.resources.Method +import io.vrap.rmf.raml.model.resources.Resource +import io.vrap.rmf.raml.model.types.ArrayType +import io.vrap.rmf.raml.model.types.ObjectType +import org.eclipse.emf.common.util.Diagnostic +import java.util.* + +@ValidatorSet +class ErrorResponseStructureRule(severity: RuleSeverity, options: List? = null) : ResourcesRule(severity, options) { + + private val exclude: List = + (options?.filter { ruleOption -> ruleOption.type.lowercase(Locale.getDefault()) == RuleOptionType.EXCLUDE.toString() }?.map { ruleOption -> ruleOption.value }?.plus("") ?: defaultExcludes) + + override fun caseMethod(method: Method): List { + val validationResults: MutableList = ArrayList() + val resource = method.eContainer() as Resource + val methodId = "${method.method.name} ${resource.fullUri.template}" + + if (exclude.contains(methodId)) { + return validationResults + } + + val errorResponses = method.responses.filter { response -> response.statusCode.toInt() >= 400 } + + errorResponses.forEach { response -> + val statusCode = response.statusCode + response.bodies.forEach { body -> + val bodyType = body.type + if (bodyType is ObjectType) { + if (bodyType.properties.none { it.name == "statusCode" }) { + validationResults.add(create(method, "Method \"{0} {1}\" error response (status {2}) body type must have a \"statusCode\" property", method.method.name, resource.fullUri.template, statusCode)) + } + + if (bodyType.properties.none { it.name == "message" }) { + validationResults.add(create(method, "Method \"{0} {1}\" error response (status {2}) body type must have a \"message\" property", method.method.name, resource.fullUri.template, statusCode)) + } + + val errorsProperty = bodyType.properties.find { it.name == "errors" } + if (errorsProperty == null) { + validationResults.add(warning(method, "Method \"{0} {1}\" error response (status {2}) body type should have an \"errors\" array property", method.method.name, resource.fullUri.template, statusCode)) + } else if (errorsProperty.type is ArrayType) { + val itemsType = (errorsProperty.type as ArrayType).items + if (itemsType is ObjectType) { + if (itemsType.properties.none { it.name == "code" }) { + validationResults.add(create(method, "Method \"{0} {1}\" error response (status {2}) error items must have a \"code\" property", method.method.name, resource.fullUri.template, statusCode)) + } + if (itemsType.properties.none { it.name == "message" }) { + validationResults.add(create(method, "Method \"{0} {1}\" error response (status {2}) error items must have a \"message\" property", method.method.name, resource.fullUri.template, statusCode)) + } + } + } + } + } + } + + return validationResults + } + + companion object : ValidatorFactory { + private val defaultExcludes by lazy { listOf("") } + + @JvmStatic + override fun create(options: List?): ErrorResponseStructureRule { + return ErrorResponseStructureRule(RuleSeverity.ERROR, options) + } + + @JvmStatic + override fun create(severity: RuleSeverity, options: List?): ErrorResponseStructureRule { + return ErrorResponseStructureRule(severity, options) + } + } +} diff --git a/ctp-validators/src/main/kotlin/com/commercetools/rmf/validators/ParameterMinMaxAbbreviationRule.kt b/ctp-validators/src/main/kotlin/com/commercetools/rmf/validators/ParameterMinMaxAbbreviationRule.kt new file mode 100644 index 00000000..202d0afe --- /dev/null +++ b/ctp-validators/src/main/kotlin/com/commercetools/rmf/validators/ParameterMinMaxAbbreviationRule.kt @@ -0,0 +1,52 @@ +package com.commercetools.rmf.validators + +import io.vrap.rmf.raml.model.resources.Method +import org.eclipse.emf.common.util.Diagnostic +import java.util.* + +@ValidatorSet +class ParameterMinMaxAbbreviationRule(severity: RuleSeverity, options: List? = null) : ResourcesRule(severity, options) { + + private val exclude: List = + (options?.filter { ruleOption -> ruleOption.type.lowercase(Locale.getDefault()) == RuleOptionType.EXCLUDE.toString() }?.map { ruleOption -> ruleOption.value }?.plus("") ?: defaultExcludes) + + override fun caseMethod(method: Method): List { + val validationResults: MutableList = ArrayList() + + method.queryParameters.forEach { queryParameter -> + run { + if (exclude.contains(queryParameter.name).not() && queryParameter.pattern == null) { + if (queryParameter.name.matches(Regex("^(minimum|maximum)([A-Z].*)?$"))) { + validationResults.add(create(queryParameter, "Query parameter \"{0}\" must use \"min\"/\"max\" instead of \"minimum\"/\"maximum\"", queryParameter.name)) + } + } + } + } + + method.headers.forEach { header -> + run { + if (exclude.contains(header.name).not()) { + if (header.name.matches(Regex("^(minimum|maximum)([A-Z].*)?$"))) { + validationResults.add(create(header, "Header \"{0}\" must use \"min\"/\"max\" instead of \"minimum\"/\"maximum\"", header.name)) + } + } + } + } + + return validationResults + } + + companion object : ValidatorFactory { + private val defaultExcludes by lazy { listOf("") } + + @JvmStatic + override fun create(options: List?): ParameterMinMaxAbbreviationRule { + return ParameterMinMaxAbbreviationRule(RuleSeverity.ERROR, options) + } + + @JvmStatic + override fun create(severity: RuleSeverity, options: List?): ParameterMinMaxAbbreviationRule { + return ParameterMinMaxAbbreviationRule(severity, options) + } + } +} diff --git a/ctp-validators/src/main/kotlin/com/commercetools/rmf/validators/PropertyMinMaxAbbreviationRule.kt b/ctp-validators/src/main/kotlin/com/commercetools/rmf/validators/PropertyMinMaxAbbreviationRule.kt new file mode 100644 index 00000000..43538fc9 --- /dev/null +++ b/ctp-validators/src/main/kotlin/com/commercetools/rmf/validators/PropertyMinMaxAbbreviationRule.kt @@ -0,0 +1,39 @@ +package com.commercetools.rmf.validators + +import io.vrap.rmf.raml.model.types.* +import org.eclipse.emf.common.util.Diagnostic +import java.util.* + +@ValidatorSet +class PropertyMinMaxAbbreviationRule(severity: RuleSeverity, options: List? = null) : TypesRule(severity, options) { + + private val exclude: List = + (options?.filter { ruleOption -> ruleOption.type.lowercase(Locale.getDefault()) == RuleOptionType.EXCLUDE.toString() }?.map { ruleOption -> ruleOption.value }?.plus("") ?: defaultExcludes) + + override fun caseObjectType(type: ObjectType): List { + val validationResults: MutableList = ArrayList() + + type.properties.forEach { property -> + if (exclude.contains("${type.name}:${property.name}").not()) { + if (property.name.matches(Regex("^(minimum|maximum)([A-Z].*)?$"))) { + validationResults.add(create(type, "Property \"{0}\" of type \"{1}\" must use \"min\"/\"max\" instead of \"minimum\"/\"maximum\"", property.name, type.name)) + } + } + } + return validationResults + } + + companion object : ValidatorFactory { + private val defaultExcludes by lazy { listOf("") } + + @JvmStatic + override fun create(options: List?): PropertyMinMaxAbbreviationRule { + return PropertyMinMaxAbbreviationRule(RuleSeverity.ERROR, options) + } + + @JvmStatic + override fun create(severity: RuleSeverity, options: List?): PropertyMinMaxAbbreviationRule { + return PropertyMinMaxAbbreviationRule(severity, options) + } + } +} diff --git a/ctp-validators/src/test/groovy/com/commercetools/rmf/validators/ValidatorRulesTest.groovy b/ctp-validators/src/test/groovy/com/commercetools/rmf/validators/ValidatorRulesTest.groovy index 8f25e415..ee6d7932 100644 --- a/ctp-validators/src/test/groovy/com/commercetools/rmf/validators/ValidatorRulesTest.groovy +++ b/ctp-validators/src/test/groovy/com/commercetools/rmf/validators/ValidatorRulesTest.groovy @@ -428,4 +428,129 @@ class ValidatorRulesTest extends Specification implements ValidatorFixtures { result.validationResults[0].message == "Type \"InvalidBaz\" has subtypes but no discriminator is set" } + def "enum value pascal case rule"() { + when: + def validators = Arrays.asList(new TypesValidator(Arrays.asList(EnumValuePascalCaseRule.create(emptyList())))) + def uri = uriFromClasspath("/enum-value-pascal-case-rule.raml") + def result = new RamlModelBuilder(validators).buildApi(uri) + then: + result.validationResults.size() == 19 + result.validationResults[0].message == "Enum value \"platform\" in type \"InvalidLowercaseEnum\" must be PascalCase" + result.validationResults[1].message == "Enum value \"external\" in type \"InvalidLowercaseEnum\" must be PascalCase" + result.validationResults[2].message == "Enum value \"disabled\" in type \"InvalidLowercaseEnum\" must be PascalCase" + result.validationResults[3].message == "Enum value \"PLATFORM\" in type \"InvalidUppercaseEnum\" must be PascalCase" + result.validationResults[4].message == "Enum value \"EXTERNAL\" in type \"InvalidUppercaseEnum\" must be PascalCase" + result.validationResults[5].message == "Enum value \"DISABLED\" in type \"InvalidUppercaseEnum\" must be PascalCase" + result.validationResults[6].message == "Enum value \"add_line_item\" in type \"InvalidSnakeCaseEnum\" must be PascalCase" + result.validationResults[7].message == "Enum value \"remove_line_item\" in type \"InvalidSnakeCaseEnum\" must be PascalCase" + result.validationResults[8].message == "Enum value \"set_custom_type\" in type \"InvalidSnakeCaseEnum\" must be PascalCase" + result.validationResults[9].message == "Enum value \"addLineItem\" in type \"InvalidCamelCaseEnum\" must be PascalCase" + result.validationResults[10].message == "Enum value \"removeLineItem\" in type \"InvalidCamelCaseEnum\" must be PascalCase" + result.validationResults[11].message == "Enum value \"setCustomType\" in type \"InvalidCamelCaseEnum\" must be PascalCase" + result.validationResults[12].message == "Enum value \"add-line-item\" in type \"InvalidKebabCaseEnum\" must be PascalCase" + result.validationResults[13].message == "Enum value \"remove-line-item\" in type \"InvalidKebabCaseEnum\" must be PascalCase" + result.validationResults[14].message == "Enum value \"invalidCamelCase\" in type \"MixedCaseEnum\" must be PascalCase" + result.validationResults[15].message == "Enum value \"INVALID_UPPER\" in type \"MixedCaseEnum\" must be PascalCase" + result.validationResults[16].message == "Enum value \"another-invalid\" in type \"MixedCaseEnum\" must be PascalCase" + result.validationResults[17].message == "Enum value \"centPrecision\" in type \"MoneyType\" must be PascalCase" + result.validationResults[18].message == "Enum value \"highPrecision\" in type \"MoneyType\" must be PascalCase" + } + + def "enum value pascal case rule with exclusions"() { + when: + def options = singletonList(new RuleOption(RuleOptionType.EXCLUDE.toString(), "InvalidLowercaseEnum:platform")) + def validators = Arrays.asList(new TypesValidator(Arrays.asList(EnumValuePascalCaseRule.create(options)))) + def uri = uriFromClasspath("/enum-value-pascal-case-rule.raml") + def result = new RamlModelBuilder(validators).buildApi(uri) + then: + result.validationResults.size() == 18 + } + def "enum value pascal case rule with type exclusion"() { + when: + def options = singletonList(new RuleOption(RuleOptionType.EXCLUDE.toString(), "MoneyType")) + def validators = Arrays.asList(new TypesValidator(Arrays.asList(EnumValuePascalCaseRule.create(options)))) + def uri = uriFromClasspath("/enum-value-pascal-case-rule.raml") + def result = new RamlModelBuilder(validators).buildApi(uri) + then: + result.validationResults.size() == 17 + } + + def "property min max abbreviation rule"() { + when: + def options = singletonList(new RuleOption(RuleOptionType.EXCLUDE.toString(), "InvalidMinMax:minimumExcluded")) + def validators = Arrays.asList(new TypesValidator(Arrays.asList(PropertyMinMaxAbbreviationRule.create(options)))) + def uri = uriFromClasspath("/property-minmax-abbreviation-rule.raml") + def result = new RamlModelBuilder(validators).buildApi(uri) + then: + result.validationResults.size() == 8 + result.validationResults[0].message == "Property \"minimumQuantity\" of type \"InvalidMinMax\" must use \"min\"/\"max\" instead of \"minimum\"/\"maximum\"" + result.validationResults[1].message == "Property \"maximumPrice\" of type \"InvalidMinMax\" must use \"min\"/\"max\" instead of \"minimum\"/\"maximum\"" + result.validationResults[2].message == "Property \"minimumOrder\" of type \"InvalidMinMax\" must use \"min\"/\"max\" instead of \"minimum\"/\"maximum\"" + result.validationResults[3].message == "Property \"maximumItems\" of type \"InvalidMinMax\" must use \"min\"/\"max\" instead of \"minimum\"/\"maximum\"" + result.validationResults[4].message == "Property \"minimum\" of type \"InvalidMinMax\" must use \"min\"/\"max\" instead of \"minimum\"/\"maximum\"" + result.validationResults[5].message == "Property \"maximum\" of type \"InvalidMinMax\" must use \"min\"/\"max\" instead of \"minimum\"/\"maximum\"" + result.validationResults[6].message == "Property \"minimumTypeNumber\" of type \"InvalidMinMax\" must use \"min\"/\"max\" instead of \"minimum\"/\"maximum\"" + result.validationResults[7].message == "Property \"minimumNumber\" of type \"InvalidMinMax\" must use \"min\"/\"max\" instead of \"minimum\"/\"maximum\"" + } + + def "property min max abbreviation rule without exclusions"() { + when: + def validators = Arrays.asList(new TypesValidator(Arrays.asList(PropertyMinMaxAbbreviationRule.create(emptyList())))) + def uri = uriFromClasspath("/property-minmax-abbreviation-rule.raml") + def result = new RamlModelBuilder(validators).buildApi(uri) + then: + result.validationResults.size() == 9 + } + + def "parameter min max abbreviation rule"() { + when: + def options = singletonList(new RuleOption(RuleOptionType.EXCLUDE.toString(), "minimumExcluded")) + def validators = Arrays.asList(new ResourcesValidator(Arrays.asList(ParameterMinMaxAbbreviationRule.create(options)))) + def uri = uriFromClasspath("/parameter-minmax-abbreviation-rule.raml") + def result = new RamlModelBuilder(validators).buildApi(uri) + then: + result.validationResults.size() == 8 + result.validationResults[0].message == "Query parameter \"minimumQuantity\" must use \"min\"/\"max\" instead of \"minimum\"/\"maximum\"" + result.validationResults[1].message == "Query parameter \"maximumPrice\" must use \"min\"/\"max\" instead of \"minimum\"/\"maximum\"" + result.validationResults[2].message == "Query parameter \"minimum\" must use \"min\"/\"max\" instead of \"minimum\"/\"maximum\"" + result.validationResults[3].message == "Query parameter \"maximum\" must use \"min\"/\"max\" instead of \"minimum\"/\"maximum\"" + result.validationResults[4].message == "Query parameter \"minimumTypeNumber\" must use \"min\"/\"max\" instead of \"minimum\"/\"maximum\"" + result.validationResults[5].message == "Query parameter \"minimumDescNumber\" must use \"min\"/\"max\" instead of \"minimum\"/\"maximum\"" + result.validationResults[6].message == "Header \"minimumRetry\" must use \"min\"/\"max\" instead of \"minimum\"/\"maximum\"" + result.validationResults[7].message == "Header \"maximumRetry\" must use \"min\"/\"max\" instead of \"minimum\"/\"maximum\"" + } + + def "parameter min max abbreviation rule without exclusions"() { + when: + def validators = Arrays.asList(new ResourcesValidator(Arrays.asList(ParameterMinMaxAbbreviationRule.create(emptyList())))) + def uri = uriFromClasspath("/parameter-minmax-abbreviation-rule.raml") + def result = new RamlModelBuilder(validators).buildApi(uri) + then: + result.validationResults.size() == 9 + } + + def "error response structure rule"() { + when: + def options = singletonList(new RuleOption(RuleOptionType.EXCLUDE.toString(), "GET /excluded")) + def validators = Arrays.asList(new ResourcesValidator(Arrays.asList(ErrorResponseStructureRule.create(options)))) + def uri = uriFromClasspath("/error-response-structure-rule.raml") + def result = new RamlModelBuilder(validators).buildApi(uri) + then: + result.validationResults.size() == 5 + result.validationResults[0].message == "Method \"GET /missing-status-code\" error response (status 400) body type must have a \"statusCode\" property" + result.validationResults[1].message == "Method \"GET /missing-message\" error response (status 404) body type must have a \"message\" property" + result.validationResults[2].message == "Method \"GET /missing-errors-array\" error response (status 500) body type should have an \"errors\" array property" + result.validationResults[2].severity == Diagnostic.WARNING + result.validationResults[3].message == "Method \"GET /invalid-error-items\" error response (status 400) error items must have a \"code\" property" + result.validationResults[4].message == "Method \"GET /invalid-error-items\" error response (status 400) error items must have a \"message\" property" + } + + def "error response structure rule without exclusions"() { + when: + def validators = Arrays.asList(new ResourcesValidator(Arrays.asList(ErrorResponseStructureRule.create(emptyList())))) + def uri = uriFromClasspath("/error-response-structure-rule.raml") + def result = new RamlModelBuilder(validators).buildApi(uri) + then: + result.validationResults.size() == 6 + } } diff --git a/ctp-validators/src/test/resources/enum-value-pascal-case-rule.raml b/ctp-validators/src/test/resources/enum-value-pascal-case-rule.raml new file mode 100644 index 00000000..3535e895 --- /dev/null +++ b/ctp-validators/src/test/resources/enum-value-pascal-case-rule.raml @@ -0,0 +1,87 @@ +#%RAML 1.0 +title: enum value pascal case rule + +annotationTypes: + package: string + +types: + # Valid enum - all values are PascalCase + ValidStatusEnum: + (package): Common + type: string + enum: + - Platform + - External + - Disabled + - Active + - Ordered + + # Valid enum - PascalCase with multiple words + ValidActionTypeEnum: + (package): Common + type: string + enum: + - AddLineItem + - RemoveLineItem + - ChangeQuantity + - SetCustomType + + # Invalid enum - lowercase values + InvalidLowercaseEnum: + (package): Common + type: string + enum: + - platform + - external + - disabled + + # Invalid enum - UPPERCASE (SCREAMING_SNAKE_CASE style) + InvalidUppercaseEnum: + (package): Common + type: string + enum: + - PLATFORM + - EXTERNAL + - DISABLED + + # Invalid enum - snake_case values + InvalidSnakeCaseEnum: + (package): Common + type: string + enum: + - add_line_item + - remove_line_item + - set_custom_type + + # Invalid enum - camelCase values (starts with lowercase) + InvalidCamelCaseEnum: + (package): Common + type: string + enum: + - addLineItem + - removeLineItem + - setCustomType + + # Invalid enum - kebab-case values + InvalidKebabCaseEnum: + (package): Common + type: string + enum: + - add-line-item + - remove-line-item + + # Mixed valid and invalid + MixedCaseEnum: + (package): Common + type: string + enum: + - ValidPascalCase + - invalidCamelCase + - INVALID_UPPER + - another-invalid + MoneyType: + (package): Common + type: string + enum: + - centPrecision + - highPrecision diff --git a/ctp-validators/src/test/resources/error-response-structure-rule.raml b/ctp-validators/src/test/resources/error-response-structure-rule.raml new file mode 100644 index 00000000..26445492 --- /dev/null +++ b/ctp-validators/src/test/resources/error-response-structure-rule.raml @@ -0,0 +1,132 @@ +#%RAML 1.0 +title: error response structure rule + +types: + ValidErrorResponse: + type: object + properties: + statusCode: + description: HTTP status code + type: integer + message: + description: Error message + type: string + errors: + type: array + items: + type: object + properties: + code: + description: Error code + type: string + message: + type: string + + MissingStatusCodeError: + type: object + properties: + message: + type: string + errors: + type: array + items: + type: object + properties: + code: + type: string + message: + type: string + + MissingMessageError: + type: object + properties: + statusCode: + description: HTTP status code + type: integer + errors: + type: array + items: + type: object + properties: + code: + type: string + message: + description: Error message + type: string + + MissingErrorsArrayError: + type: object + properties: + statusCode: + type: integer + message: + type: string + + InvalidErrorItems: + type: object + properties: + statusCode: + type: integer + message: + description: Error message + type: string + errors: + type: array + items: + type: object + properties: + detail: + description: Error detail + type: string + +/valid: + get: + responses: + 200: + body: + application/json: + type: object + 400: + body: + application/json: + type: ValidErrorResponse + +/missing-status-code: + get: + responses: + 400: + body: + application/json: + type: MissingStatusCodeError + +/missing-message: + get: + responses: + 404: + body: + application/json: + type: MissingMessageError + +/missing-errors-array: + get: + responses: + 500: + body: + application/json: + type: MissingErrorsArrayError + +/invalid-error-items: + get: + responses: + 400: + body: + application/json: + type: InvalidErrorItems + +/excluded: + get: + responses: + 400: + body: + application/json: + type: MissingStatusCodeError diff --git a/ctp-validators/src/test/resources/parameter-minmax-abbreviation-rule.raml b/ctp-validators/src/test/resources/parameter-minmax-abbreviation-rule.raml new file mode 100644 index 00000000..2ae16743 --- /dev/null +++ b/ctp-validators/src/test/resources/parameter-minmax-abbreviation-rule.raml @@ -0,0 +1,22 @@ +#%RAML 1.0 +title: parameter min max abbreviation rule + +/resources: + get: + queryParameters: + minimumQuantity: string + maximumPrice: string + minimum: string + maximum: string + minimumExcluded: string + minimumTypeNumber: + type: string + minimumDescNumber: + description: Lorem Ipsum + type: string + minQuantity: string + maxPrice: string + headers: + minimumRetry: string + maximumRetry: string + maxRetry: string diff --git a/ctp-validators/src/test/resources/property-minmax-abbreviation-rule.raml b/ctp-validators/src/test/resources/property-minmax-abbreviation-rule.raml new file mode 100644 index 00000000..a1c9c2ac --- /dev/null +++ b/ctp-validators/src/test/resources/property-minmax-abbreviation-rule.raml @@ -0,0 +1,61 @@ +#%RAML 1.0 +title: property min max abbreviation rule + +annotationTypes: + package: string + sdkBaseUri: string + +baseUri: https://api.europe-west1.commercetools.com + +types: + InvalidMinMax: + (package): Common + type: object + properties: + minimumQuantity: + description: uses minimum as prefix - should be minQuantity + type: number + maximumPrice: + description: uses maximum as prefix - should be maxPrice + type: number + minimumOrder: + description: uses minimum as prefix - should be minOrder + type: number + maximumItems: + description: uses maximum as prefix - should be maxItems + type: number + minimum: + description: standalone minimum - should be min + type: number + maximum: + description: standalone maximum - should be max + type: number + minimumExcluded: + description: will be excluded via options + type: string + minimumTypeNumber: + type: number + minimumNumber: number + + ValidMinMax: + (package): Common + type: object + properties: + minQuantity: + description: correct abbreviated prefix + type: number + maxPrice: + description: correct abbreviated prefix + type: number + min: + description: correct standalone abbreviation + type: number + max: + description: correct standalone abbreviation + type: number + validName: + description: unrelated property name + type: string + minNumber: number + minTypeNumber: + type: number diff --git a/languages/csharp/src/main/kotlin/io/vrap/codegen/languages/csharp/extensions/CsharpVrapExtensions.kt b/languages/csharp/src/main/kotlin/io/vrap/codegen/languages/csharp/extensions/CsharpVrapExtensions.kt index d15f27bd..a73e8791 100644 --- a/languages/csharp/src/main/kotlin/io/vrap/codegen/languages/csharp/extensions/CsharpVrapExtensions.kt +++ b/languages/csharp/src/main/kotlin/io/vrap/codegen/languages/csharp/extensions/CsharpVrapExtensions.kt @@ -168,7 +168,7 @@ fun QueryParameter.methodName(): String { val paramName = o.value.stream().filter { propertyValue -> propertyValue.name == "paramName" }.findFirst().orElse(null).value as StringInstance return "With" + StringCaseFormat.UPPER_CAMEL_CASE.apply(paramName.value) } - return "With" + StringCaseFormat.UPPER_CAMEL_CASE.apply(this.name.replace(".", "-")) + return "With" + StringCaseFormat.UPPER_CAMEL_CASE.apply(this.name.replace(".", "-").replace("[", "-").replace("]", "")) } fun Property.deprecated() : Boolean { @@ -188,4 +188,3 @@ fun Property.markDeprecated() : Boolean { val typeAnno = this.type.getAnnotation("markDeprecated") return (typeAnno != null && (typeAnno.value as BooleanInstance).value) } - diff --git a/languages/csharp/src/main/kotlin/io/vrap/codegen/languages/csharp/model/CsharpTraitRenderer.kt b/languages/csharp/src/main/kotlin/io/vrap/codegen/languages/csharp/model/CsharpTraitRenderer.kt index d441c145..7c55fc64 100644 --- a/languages/csharp/src/main/kotlin/io/vrap/codegen/languages/csharp/model/CsharpTraitRenderer.kt +++ b/languages/csharp/src/main/kotlin/io/vrap/codegen/languages/csharp/model/CsharpTraitRenderer.kt @@ -109,6 +109,6 @@ class CsharpTraitRenderer constructor(override val vrapTypeProvider: VrapTypePr } private fun QueryParameter.fieldName(): String { - return StringCaseFormat.LOWER_CAMEL_CASE.apply(this.name.replace(".", "-")) + return StringCaseFormat.LOWER_CAMEL_CASE.apply(this.name.replace(".", "-").replace("[", "-").replace("]", "")) } } diff --git a/languages/csharp/src/main/kotlin/io/vrap/codegen/languages/csharp/requests/CsharpHttpRequestRenderer.kt b/languages/csharp/src/main/kotlin/io/vrap/codegen/languages/csharp/requests/CsharpHttpRequestRenderer.kt index a41bb2d4..09716452 100644 --- a/languages/csharp/src/main/kotlin/io/vrap/codegen/languages/csharp/requests/CsharpHttpRequestRenderer.kt +++ b/languages/csharp/src/main/kotlin/io/vrap/codegen/languages/csharp/requests/CsharpHttpRequestRenderer.kt @@ -187,7 +187,7 @@ class CsharpHttpRequestRenderer constructor(override val vrapTypeProvider: VrapT } private fun QueryParameter.fieldName(): String { - return StringCaseFormat.LOWER_CAMEL_CASE.apply(this.name.replace(".", "-")) + return StringCaseFormat.LOWER_CAMEL_CASE.apply(this.name.replace(".", "-").replace("[", "-").replace("]", "")) } private fun QueryParameter.fieldNameAsString(type: String): String { diff --git a/languages/csharp/src/main/kotlin/io/vrap/codegen/languages/csharp/test/CsharpRequestTestRenderer.kt b/languages/csharp/src/main/kotlin/io/vrap/codegen/languages/csharp/test/CsharpRequestTestRenderer.kt index 4eb099da..85b13b4d 100644 --- a/languages/csharp/src/main/kotlin/io/vrap/codegen/languages/csharp/test/CsharpRequestTestRenderer.kt +++ b/languages/csharp/src/main/kotlin/io/vrap/codegen/languages/csharp/test/CsharpRequestTestRenderer.kt @@ -14,6 +14,7 @@ import io.vrap.rmf.codegen.types.VrapTypeProvider import io.vrap.rmf.raml.model.resources.Method import io.vrap.rmf.raml.model.resources.Resource import io.vrap.rmf.raml.model.types.* +import java.net.URLEncoder import kotlin.random.Random class CsharpRequestTestRenderer constructor(override val vrapTypeProvider: VrapTypeProvider, private val basePackagePrefix: String): ResourceRenderer, CsharpEObjectTypeExtensions, CsharpObjectTypeExtensions { @@ -135,7 +136,7 @@ class CsharpRequestTestRenderer constructor(override val vrapTypeProvider: VrapT NumberFormat.FLOAT -> r.nextFloat().toString() else -> r.nextInt(1, 10) } - else -> name + else -> URLEncoder.encode(name, "UTF-8") } } diff --git a/languages/go/src/main/kotlin/io/vrap/codegen/languages/go/GoStringExtension.kt b/languages/go/src/main/kotlin/io/vrap/codegen/languages/go/GoStringExtension.kt index 180515f6..3bc07a34 100644 --- a/languages/go/src/main/kotlin/io/vrap/codegen/languages/go/GoStringExtension.kt +++ b/languages/go/src/main/kotlin/io/vrap/codegen/languages/go/GoStringExtension.kt @@ -41,7 +41,7 @@ fun String.exportName(): String { if (this[0].isUpperCase()) { return this } - var name = StringCaseFormat.UPPER_CAMEL_CASE.apply(this.replace(".", "_")) + var name = StringCaseFormat.UPPER_CAMEL_CASE.apply(this.replace(Regex("[.\\[\\]]"), "_")) mapOf( "^Id$" to "ID" ).forEach { (key, value) -> name = name.replace(key.toRegex(), value) } diff --git a/languages/javalang/builder-renderer/java-builder-client/src/main/kotlin/io/vrap/codegen/languages/javalang/client/builder/model/JavaModelInterfaceRenderer.kt b/languages/javalang/builder-renderer/java-builder-client/src/main/kotlin/io/vrap/codegen/languages/javalang/client/builder/model/JavaModelInterfaceRenderer.kt index e19828e9..0904931a 100644 --- a/languages/javalang/builder-renderer/java-builder-client/src/main/kotlin/io/vrap/codegen/languages/javalang/client/builder/model/JavaModelInterfaceRenderer.kt +++ b/languages/javalang/builder-renderer/java-builder-client/src/main/kotlin/io/vrap/codegen/languages/javalang/client/builder/model/JavaModelInterfaceRenderer.kt @@ -50,7 +50,7 @@ class JavaModelInterfaceRenderer constructor(override val vrapTypeProvider: Vrap |${type.subclassImport()} | |import com.fasterxml.jackson.annotation.*; - |import com.fasterxml.jackson.databind.annotation.*; + |import tools.jackson.databind.annotation.*; |import io.vrap.rmf.base.client.utils.Generated; |import io.vrap.rmf.base.client.Accessor; |import jakarta.validation.Valid; @@ -118,8 +118,8 @@ class JavaModelInterfaceRenderer constructor(override val vrapTypeProvider: Vrap | * gives a TypeReference for usage with Jackson DataBind | * @return TypeReference | */ - |public static com.fasterxml.jackson.core.type.TypeReference<${vrapType.simpleClassName}> typeReference() { - | return new com.fasterxml.jackson.core.type.TypeReference<${vrapType.simpleClassName}>() { + |public static tools.jackson.core.type.TypeReference<${vrapType.simpleClassName}> typeReference() { + | return new tools.jackson.core.type.TypeReference<${vrapType.simpleClassName}>() { | @Override | public String toString() { | return "TypeReference<${vrapType.simpleClassName}>"; diff --git a/languages/javalang/builder-renderer/java-builder-client/src/main/kotlin/io/vrap/codegen/languages/javalang/client/builder/model/JavaTraitRenderer.kt b/languages/javalang/builder-renderer/java-builder-client/src/main/kotlin/io/vrap/codegen/languages/javalang/client/builder/model/JavaTraitRenderer.kt index 1666d0ab..5e4c2ff7 100644 --- a/languages/javalang/builder-renderer/java-builder-client/src/main/kotlin/io/vrap/codegen/languages/javalang/client/builder/model/JavaTraitRenderer.kt +++ b/languages/javalang/builder-renderer/java-builder-client/src/main/kotlin/io/vrap/codegen/languages/javalang/client/builder/model/JavaTraitRenderer.kt @@ -146,6 +146,6 @@ class JavaTraitRenderer constructor(override val vrapTypeProvider: VrapTypeProvi } private fun QueryParameter.fieldName(): String { - return StringCaseFormat.LOWER_CAMEL_CASE.apply(this.name.replace(".", "-")) + return StringCaseFormat.LOWER_CAMEL_CASE.apply(this.name.replace(".", "-").replace("[", "-").replace("]", "")) } } diff --git a/languages/javalang/builder-renderer/java-builder-client/src/main/kotlin/io/vrap/codegen/languages/javalang/client/builder/producers/JavaModelClassFileProducer.kt b/languages/javalang/builder-renderer/java-builder-client/src/main/kotlin/io/vrap/codegen/languages/javalang/client/builder/producers/JavaModelClassFileProducer.kt index cbc5dc00..44813ca9 100644 --- a/languages/javalang/builder-renderer/java-builder-client/src/main/kotlin/io/vrap/codegen/languages/javalang/client/builder/producers/JavaModelClassFileProducer.kt +++ b/languages/javalang/builder-renderer/java-builder-client/src/main/kotlin/io/vrap/codegen/languages/javalang/client/builder/producers/JavaModelClassFileProducer.kt @@ -50,8 +50,8 @@ class JavaModelClassFileProducer constructor(override val vrapTypeProvider: Vrap |import java.util.*; |import java.time.*; | - |import com.fasterxml.jackson.core.JsonProcessingException; - |import com.fasterxml.jackson.databind.annotation.*; + |import tools.jackson.core.exc.JacksonException; + |import import tools.jackson.databind.annotation.*; |import com.fasterxml.jackson.annotation.JsonAnySetter; |import com.fasterxml.jackson.annotation.JsonCreator; |import com.fasterxml.jackson.annotation.JsonIgnore; diff --git a/languages/javalang/builder-renderer/java-builder-client/src/main/kotlin/io/vrap/codegen/languages/javalang/client/builder/requests/JavaHttpRequestRenderer.kt b/languages/javalang/builder-renderer/java-builder-client/src/main/kotlin/io/vrap/codegen/languages/javalang/client/builder/requests/JavaHttpRequestRenderer.kt index fe5c252f..a3aa8b92 100644 --- a/languages/javalang/builder-renderer/java-builder-client/src/main/kotlin/io/vrap/codegen/languages/javalang/client/builder/requests/JavaHttpRequestRenderer.kt +++ b/languages/javalang/builder-renderer/java-builder-client/src/main/kotlin/io/vrap/codegen/languages/javalang/client/builder/requests/JavaHttpRequestRenderer.kt @@ -78,7 +78,7 @@ class JavaHttpRequestRenderer constructor(override val vrapTypeProvider: VrapTyp |import java.util.stream.Collectors; |import java.util.concurrent.CompletableFuture; |import io.vrap.rmf.base.client.utils.Generated; - |import com.fasterxml.jackson.core.type.TypeReference; + |import tools.jackson.core.type.TypeReference; | |import javax.annotation.Nullable; | @@ -313,7 +313,7 @@ class JavaHttpRequestRenderer constructor(override val vrapTypeProvider: VrapTyp } private fun QueryParameter.fieldName(): String { - return StringCaseFormat.LOWER_CAMEL_CASE.apply(this.name.replace(".", "-")) + return StringCaseFormat.LOWER_CAMEL_CASE.apply(this.name.replace(".", "-").replace("[", "-").replace("]", "")) } private fun Method.pathArguments() : List { diff --git a/languages/javalang/builder-renderer/java-builder-client/src/main/kotlin/io/vrap/codegen/languages/javalang/client/builder/requests/JavaStringHttpRequestRenderer.kt b/languages/javalang/builder-renderer/java-builder-client/src/main/kotlin/io/vrap/codegen/languages/javalang/client/builder/requests/JavaStringHttpRequestRenderer.kt index 2a41e47d..dfc3c582 100644 --- a/languages/javalang/builder-renderer/java-builder-client/src/main/kotlin/io/vrap/codegen/languages/javalang/client/builder/requests/JavaStringHttpRequestRenderer.kt +++ b/languages/javalang/builder-renderer/java-builder-client/src/main/kotlin/io/vrap/codegen/languages/javalang/client/builder/requests/JavaStringHttpRequestRenderer.kt @@ -73,7 +73,7 @@ class JavaStringHttpRequestRenderer constructor(override val vrapTypeProvider: V |import java.util.stream.Collectors; |import java.util.concurrent.CompletableFuture; |import io.vrap.rmf.base.client.utils.Generated; - |import com.fasterxml.jackson.core.type.TypeReference; + |import tools.jackson.core.type.TypeReference; | |import javax.annotation.Nullable; | diff --git a/languages/javalang/builder-renderer/java-builder-client/src/main/kotlin/io/vrap/codegen/languages/javalang/client/builder/test/JavaRequestTestRenderer.kt b/languages/javalang/builder-renderer/java-builder-client/src/main/kotlin/io/vrap/codegen/languages/javalang/client/builder/test/JavaRequestTestRenderer.kt index ccdb8f3c..24b09e27 100644 --- a/languages/javalang/builder-renderer/java-builder-client/src/main/kotlin/io/vrap/codegen/languages/javalang/client/builder/test/JavaRequestTestRenderer.kt +++ b/languages/javalang/builder-renderer/java-builder-client/src/main/kotlin/io/vrap/codegen/languages/javalang/client/builder/test/JavaRequestTestRenderer.kt @@ -14,6 +14,7 @@ import io.vrap.rmf.codegen.types.VrapTypeProvider import io.vrap.rmf.raml.model.resources.Method import io.vrap.rmf.raml.model.resources.Resource import io.vrap.rmf.raml.model.types.* +import java.net.URLEncoder import kotlin.random.Random class JavaRequestTestRenderer constructor(override val vrapTypeProvider: VrapTypeProvider): ResourceRenderer, JavaEObjectTypeExtensions, JavaObjectTypeExtensions { @@ -221,7 +222,7 @@ class JavaRequestTestRenderer constructor(override val vrapTypeProvider: VrapTyp NumberFormat.FLOAT -> r.nextFloat() else -> r.nextInt(1, 10) } - else -> name + else -> URLEncoder.encode(name, "UTF-8") } } diff --git a/languages/javalang/java-base/src/main/kotlin/io/vrap/codegen/languages/java/base/extensions/JavaMethodExtensions.kt b/languages/javalang/java-base/src/main/kotlin/io/vrap/codegen/languages/java/base/extensions/JavaMethodExtensions.kt index 688896de..cd223272 100644 --- a/languages/javalang/java-base/src/main/kotlin/io/vrap/codegen/languages/java/base/extensions/JavaMethodExtensions.kt +++ b/languages/javalang/java-base/src/main/kotlin/io/vrap/codegen/languages/java/base/extensions/JavaMethodExtensions.kt @@ -13,7 +13,7 @@ fun Method.javaReturnType(vrapTypeProvider: VrapTypeProvider) : String { if(returnType is VrapObjectType) { return "${returnType.`package`.toJavaPackage()}.${returnType.simpleClassName}" } - return "com.fasterxml.jackson.databind.JsonNode" + return "tools.jackson.databind.JsonNode" } fun Method.toStringRequestName(): String { diff --git a/languages/javalang/java-base/src/main/kotlin/io/vrap/codegen/languages/java/base/extensions/JavaVrapExtensions.kt b/languages/javalang/java-base/src/main/kotlin/io/vrap/codegen/languages/java/base/extensions/JavaVrapExtensions.kt index 8b59c901..7f4f43e3 100644 --- a/languages/javalang/java-base/src/main/kotlin/io/vrap/codegen/languages/java/base/extensions/JavaVrapExtensions.kt +++ b/languages/javalang/java-base/src/main/kotlin/io/vrap/codegen/languages/java/base/extensions/JavaVrapExtensions.kt @@ -116,5 +116,5 @@ fun QueryParameter.methodName(): String { val paramName = o.value.stream().filter { propertyValue -> propertyValue.name == "paramName" }.findFirst().orElse(null).value as StringInstance return "with" + StringCaseFormat.UPPER_CAMEL_CASE.apply(paramName.value) } - return "with" + StringCaseFormat.UPPER_CAMEL_CASE.apply(this.name.replace(".", "-")) + return "with" + StringCaseFormat.UPPER_CAMEL_CASE.apply(this.name.replace(".", "-").replace("[", "-").replace("]", "")) } diff --git a/languages/php/src/main/kotlin/io/vrap/codegen/languages/php/model/PhpMethodRenderer.kt b/languages/php/src/main/kotlin/io/vrap/codegen/languages/php/model/PhpMethodRenderer.kt index b0d717d7..9f62d839 100644 --- a/languages/php/src/main/kotlin/io/vrap/codegen/languages/php/model/PhpMethodRenderer.kt +++ b/languages/php/src/main/kotlin/io/vrap/codegen/languages/php/model/PhpMethodRenderer.kt @@ -111,7 +111,7 @@ class PhpMethodRenderer constructor(override val vrapTypeProvider: VrapTypeProvi | $!uri = str_replace([${ type.allParams()?.joinToString(separator = ", ") { "'{$it}'" } ?: "" }], [${ - type.allParams()?.joinToString(separator = ", ") { "$$it" } ?: "" + type.allParams()?.joinToString(separator = ", ") { "urlencode($$it)" } ?: "" }], '${type.apiResource().fullUri.template.trimStart('/')}'); | <<${type.firstBody()?.ensureContentType() ?: ""}>> | <<${ diff --git a/languages/php/src/main/kotlin/io/vrap/codegen/languages/php/model/PhpTraitRenderer.kt b/languages/php/src/main/kotlin/io/vrap/codegen/languages/php/model/PhpTraitRenderer.kt index 61352e43..bcd7c23f 100644 --- a/languages/php/src/main/kotlin/io/vrap/codegen/languages/php/model/PhpTraitRenderer.kt +++ b/languages/php/src/main/kotlin/io/vrap/codegen/languages/php/model/PhpTraitRenderer.kt @@ -77,7 +77,7 @@ class PhpTraitRenderer constructor(override val vrapTypeProvider: VrapTypeProvid } private fun QueryParameter.fieldName(): String { - return StringCaseFormat.LOWER_CAMEL_CASE.apply(this.name.replace(".", "-")) + return StringCaseFormat.LOWER_CAMEL_CASE.apply(this.name.replace(".", "-").replace("[", "-").replace("]", "")) } private fun Trait.markDeprecated() : Boolean { diff --git a/languages/php/src/main/kotlin/io/vrap/codegen/languages/php/test/PhpRequestTestRenderer.kt b/languages/php/src/main/kotlin/io/vrap/codegen/languages/php/test/PhpRequestTestRenderer.kt index 8af4405a..1486457c 100644 --- a/languages/php/src/main/kotlin/io/vrap/codegen/languages/php/test/PhpRequestTestRenderer.kt +++ b/languages/php/src/main/kotlin/io/vrap/codegen/languages/php/test/PhpRequestTestRenderer.kt @@ -19,6 +19,7 @@ import io.vrap.rmf.raml.model.types.ObjectInstance import io.vrap.rmf.raml.model.types.QueryParameter import io.vrap.rmf.raml.model.types.StringInstance import org.eclipse.emf.ecore.EObject +import java.net.URLEncoder class PhpRequestTestRenderer constructor(api: Api, vrapTypeProvider: VrapTypeProvider, clientConstants: ClientConstants) : ResourceRenderer, AbstractRequestBuilder(api, vrapTypeProvider, clientConstants), EObjectTypeExtensions { private val resourcePackage = "Resource" @@ -243,7 +244,7 @@ class PhpRequestTestRenderer constructor(api: Api, vrapTypeProvider: VrapTypePro | <<${builderChain.joinToString("\n->", "->")}>>; | }, | '${method.method}', - | '${resource.fullUri.expand(resource.fullUriParameters.map { it.name to "test_${it.name}" }.toMap()).trimStart('/')}?${paramName}=${paramName}', + | '${resource.fullUri.expand(resource.fullUriParameters.map { it.name to "test_${it.name}" }.toMap()).trimStart('/')}?${URLEncoder.encode(paramName, "UTF-8")}=${URLEncoder.encode(paramName, "UTF-8")}', |] """.trimMargin() } diff --git a/languages/typescript/src/main/kotlin/io/vrap/codegen/languages/typescript/joi/JoiValidatorModuleRenderer.kt b/languages/typescript/src/main/kotlin/io/vrap/codegen/languages/typescript/joi/JoiValidatorModuleRenderer.kt index 216948b0..c7898ab6 100644 --- a/languages/typescript/src/main/kotlin/io/vrap/codegen/languages/typescript/joi/JoiValidatorModuleRenderer.kt +++ b/languages/typescript/src/main/kotlin/io/vrap/codegen/languages/typescript/joi/JoiValidatorModuleRenderer.kt @@ -8,6 +8,7 @@ import io.vrap.codegen.languages.typescript.tsGeneratedComment import io.vrap.rmf.codegen.di.AllAnyTypes import io.vrap.rmf.codegen.io.TemplateFile import io.vrap.rmf.codegen.rendering.FileProducer +import io.vrap.rmf.codegen.rendering.utils.escapeAll import io.vrap.rmf.codegen.rendering.utils.keepIndentation import io.vrap.rmf.codegen.types.* import io.vrap.rmf.raml.model.types.* @@ -194,7 +195,13 @@ class JoiValidatorModuleRenderer constructor(override val vrapTypeProvider: Vrap vrapType.renderTypeRef() val uniqueAnnotation = this.getAnnotation("uniqueProperty") ?: this.type.getAnnotation("uniqueProperty") val uniquePropConstraint = if (uniqueAnnotation != null) ".unique(\"${(uniqueAnnotation.value as StringInstance).value}\")" else "" - return "${name}: ${joiSchema}${minConstraint}${maxConstraint}${patternConstraint}${discriminatorConstraint}${uniquePropConstraint}${requiredConstraint}" + val uniquePropsAnnotation = this.getAnnotation("uniqueProperties") ?: this.type.getAnnotation("uniqueProperties") + val uniquePropsConstraint = if (uniquePropsAnnotation != null) ".unique((a, b) => ${ + (uniquePropsAnnotation.value as ArrayInstance).value.joinToString( + " && " + ) { "a[\"${it.value}\"] === b[\"${it.value}\"]" } + })".escapeAll() else "" + return "${name}: ${joiSchema}${minConstraint}${maxConstraint}${patternConstraint}${discriminatorConstraint}${uniquePropConstraint}${uniquePropsConstraint}${requiredConstraint}" } private fun VrapType.renderTypeRef(): String { diff --git a/languages/typescript/src/main/kotlin/io/vrap/codegen/languages/typescript/test/TypescriptRequestTestRenderer.kt b/languages/typescript/src/main/kotlin/io/vrap/codegen/languages/typescript/test/TypescriptRequestTestRenderer.kt index 3e007975..d07ab82a 100644 --- a/languages/typescript/src/main/kotlin/io/vrap/codegen/languages/typescript/test/TypescriptRequestTestRenderer.kt +++ b/languages/typescript/src/main/kotlin/io/vrap/codegen/languages/typescript/test/TypescriptRequestTestRenderer.kt @@ -15,6 +15,8 @@ import io.vrap.rmf.codegen.types.VrapTypeProvider import io.vrap.rmf.raml.model.resources.Method import io.vrap.rmf.raml.model.resources.Resource import io.vrap.rmf.raml.model.types.* +import io.vrap.rmf.raml.model.util.StringCaseFormat +import java.net.URLEncoder import java.util.regex.Pattern import kotlin.random.Random @@ -90,7 +92,7 @@ class TypescriptRequestTestRenderer constructor(override val vrapTypeProvider: V var methodValue = parameter.template() var requiredParams = method.queryParameters.toMutableList().filter { p -> p.name != parameter.name && p.required } var requiredParamsStr: String = requiredParams.map { r -> "${r.name}:${queryParamValueString(r.name, r.type, Random(r.name.hashCode()))}" }.joinToString(", ") - var requiredParamsStrUrl: String = requiredParams.map { r -> "${r.name}:${queryParamValueString(r.name, r.type, Random(r.name.hashCode())).toString().replace("\"","")}" }.joinToString("&") + var requiredParamsStrUrl: String = requiredParams.map { r -> "${r.name}:${URLEncoder.encode(queryParamValueString(r.name, r.type, Random(r.name.hashCode())).toString().replace("\"",""), "UTF-8")}" }.joinToString("&") if (anno != null) { val o = anno.value as ObjectInstance @@ -99,7 +101,7 @@ class TypescriptRequestTestRenderer constructor(override val vrapTypeProvider: V paramName = "\"${placeholderTemplate.value.replace("<${placeholder.value}>", placeholder.value)}\"" methodValue = "$paramName" } - else if(paramName.contains(".")) + else if(paramName.contains(".") || paramName.contains("[")) { paramName = "\"${paramName}\"" } @@ -110,7 +112,7 @@ class TypescriptRequestTestRenderer constructor(override val vrapTypeProvider: V return """ |{ | method: '${method.method}', - | uri: '/${resource.fullUri.expand(resource.fullUriParameters.map { it.name to "test_${it.name}" }.toMap()).trimStart('/')}?${paramName.replace("\"", "")}=${queryParamValueString(paramName, parameter.type, Random(paramName.hashCode())).toString().replace("\"","")}${ if(requiredParamsStrUrl!="") "&${requiredParamsStrUrl.replace(":","=")}" else ""}', + | uri: '/${resource.fullUri.expand(resource.fullUriParameters.map { it.name to "test_${it.name}" }.toMap()).trimStart('/')}?${URLEncoder.encode(paramName.replace("\"", ""), "UTF-8")}=${URLEncoder.encode(queryParamValueString(paramName, parameter.type, Random(paramName.hashCode())).toString().replace("\"",""), "UTF-8")}${ if(requiredParamsStrUrl!="") "&${requiredParamsStrUrl.replace(":","=")}" else ""}', | request: apiRoot | <<${builderChain.joinToString("\n.", ".")}>>, |} diff --git a/node/rmf-codegen/package.json b/node/rmf-codegen/package.json index c9f5f412..af44ad3c 100644 --- a/node/rmf-codegen/package.json +++ b/node/rmf-codegen/package.json @@ -1,6 +1,6 @@ { "name": "@commercetools-docs/rmf-codegen", - "version": "13.48.0", + "version": "13.51.0", "description": "Provides RMF-Codegen to javascript projects", "license": "MIT", "homepage": "https://github.com/commercetools/rmf-codegen", diff --git a/scripts/install.sh b/scripts/install.sh index 5a0ff11e..1b0f6c17 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -2,7 +2,7 @@ set -e -CODEGEN_VERSION=${VRAP_VERSION:-"1.0.0-20250827125749"} +CODEGEN_VERSION=${VRAP_VERSION:-"1.0.0-20260319131529"} CLI_HOME=~/.rmf-cli LIB_FOLDER=$CLI_HOME/lib JAR_FILE_PATH=$LIB_FOLDER/codegen-cli-${CODEGEN_VERSION}.jar