From 7f5465686773211d3aa1999096b3eaa3571573d1 Mon Sep 17 00:00:00 2001 From: FFawzy Date: Tue, 17 Feb 2026 20:39:38 +0100 Subject: [PATCH 1/3] feat: agent init draft 1 --- .../ctp-validator-rule-creator.agent.md | 224 ++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 .github/agents/ctp-validator-rule-creator.agent.md 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..e0fac653 --- /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 the rule file**: `YourRuleNameRule.kt` in the validators directory +3. **Create the test RAML**: `your-rule-name.raml` in test resources +4. **Add the test case**: Add test method to `ValidatorRulesTest.groovy` +5. **Run tests**: Use `./gradlew :ctp-validators:test` to verify +6. **Document**: Ensure error messages are clear and helpful + +## 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 From 1bfb626f8016bfc9aa772d11a6b66e3527c0495f Mon Sep 17 00:00:00 2001 From: FFawzy Date: Tue, 17 Feb 2026 21:12:28 +0100 Subject: [PATCH 2/3] test: adding a new EnumValuePascalCaseRule --- .../rmf/validators/EnumValuePascalCaseRule.kt | 66 +++++++++++++++ .../rmf/validators/ValidatorRulesTest.groovy | 36 +++++++++ .../enum-value-pascal-case-rule.raml | 81 +++++++++++++++++++ 3 files changed, 183 insertions(+) create mode 100644 ctp-validators/src/main/kotlin/com/commercetools/rmf/validators/EnumValuePascalCaseRule.kt create mode 100644 ctp-validators/src/test/resources/enum-value-pascal-case-rule.raml 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..ce3bf2a3 --- /dev/null +++ b/ctp-validators/src/main/kotlin/com/commercetools/rmf/validators/EnumValuePascalCaseRule.kt @@ -0,0 +1,66 @@ +package com.commercetools.rmf.validators + +import io.vrap.rmf.raml.model.types.StringType +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 (exclude.contains(type.name).not() && type.name != "string" && type.enum.isNullOrEmpty().not()) { + type.enum.forEach { enumValue -> + val enumName = enumValue.value as? String + if (enumName != null && !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 { + if (value.isEmpty()) return false + + // Must start with uppercase letter + if (!value[0].isUpperCase()) return false + + // Should not contain underscores or hyphens (common in snake_case or kebab-case) + if (value.contains('_') || value.contains('-')) return false + + // Should not be all uppercase (SCREAMING_SNAKE_CASE) + if (value.length > 1 && value.all { it.isUpperCase() || !it.isLetter() }) return false + + // Check that it only contains letters (PascalCase should not have numbers at the start or special characters) + // Allow letters and numbers, but must start with uppercase letter + return value.all { it.isLetterOrDigit() } + } + + 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/test/groovy/com/commercetools/rmf/validators/ValidatorRulesTest.groovy b/ctp-validators/src/test/groovy/com/commercetools/rmf/validators/ValidatorRulesTest.groovy index 8f25e415..6dc65aed 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,40 @@ 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() == 17 + 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" + } + + def "enum value pascal case rule with exclusions"() { + when: + def options = singletonList(new RuleOption(RuleOptionType.EXCLUDE.toString(), "InvalidLowercaseEnum")) + 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() == 14 + } + } 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..1a03f32a --- /dev/null +++ b/ctp-validators/src/test/resources/enum-value-pascal-case-rule.raml @@ -0,0 +1,81 @@ +#%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 From 22fbd305b0fad796ed3342d062f86fc9a374f4cd Mon Sep 17 00:00:00 2001 From: FFawzy Date: Wed, 18 Feb 2026 17:45:46 +0100 Subject: [PATCH 3/3] update agent with documentation step --- .github/agents/ctp-validator-rule-creator.agent.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/agents/ctp-validator-rule-creator.agent.md b/.github/agents/ctp-validator-rule-creator.agent.md index e0fac653..24cfd52f 100644 --- a/.github/agents/ctp-validator-rule-creator.agent.md +++ b/.github/agents/ctp-validator-rule-creator.agent.md @@ -192,11 +192,11 @@ private fun AnyType.isBoolean(): Boolean { ## Workflow for Creating a New Rule 1. **Understand the requirement**: Clarify what the rule should validate -2. **Create the rule file**: `YourRuleNameRule.kt` in the validators directory -3. **Create the test RAML**: `your-rule-name.raml` in test resources +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**: Ensure error messages are clear and helpful +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