From eed094cb2b8c26bc853c3fa095962ce01cb934cd Mon Sep 17 00:00:00 2001 From: Mo Alras Date: Wed, 4 Mar 2026 15:57:24 +0100 Subject: [PATCH 1/2] Add WithTotalDefaultRule validation rule Enforce that the withTotal query parameter, if present, must have a default value of false to optimize query performance. Co-Authored-By: Claude Opus 4.6 --- .../rmf/validators/WithTotalDefaultRule.kt | 63 +++++++++++++++++++ .../rmf/validators/ValidatorRulesTest.groovy | 11 ++++ .../resources/with-total-default-rule.raml | 32 ++++++++++ 3 files changed, 106 insertions(+) create mode 100644 ctp-validators/src/main/kotlin/com/commercetools/rmf/validators/WithTotalDefaultRule.kt create mode 100644 ctp-validators/src/test/resources/with-total-default-rule.raml diff --git a/ctp-validators/src/main/kotlin/com/commercetools/rmf/validators/WithTotalDefaultRule.kt b/ctp-validators/src/main/kotlin/com/commercetools/rmf/validators/WithTotalDefaultRule.kt new file mode 100644 index 00000000..10796724 --- /dev/null +++ b/ctp-validators/src/main/kotlin/com/commercetools/rmf/validators/WithTotalDefaultRule.kt @@ -0,0 +1,63 @@ +package com.commercetools.rmf.validators + +import io.vrap.rmf.raml.model.resources.Method +import io.vrap.rmf.raml.model.resources.Resource +import org.eclipse.emf.common.util.Diagnostic +import java.util.* + +@ValidatorSet +class WithTotalDefaultRule(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 (queryParameter.name == "withTotal") { + val resourcePath = (method.eContainer() as Resource).fullUri.template + if (exclude.contains("${method.method.name} $resourcePath").not()) { + val defaultValue = queryParameter.type.default + if (defaultValue == null) { + validationResults.add( + create( + queryParameter, + "Query parameter \"withTotal\" of method \"{0} {1}\" must have a default value of \"false\"", + method.method.name, + resourcePath + ) + ) + } else if (defaultValue.value.toString() != "false") { + validationResults.add( + create( + queryParameter, + "Query parameter \"withTotal\" of method \"{0} {1}\" must have a default value of \"false\", found \"{2}\"", + method.method.name, + resourcePath, + defaultValue.value.toString() + ) + ) + } + } + } + } + } + return validationResults + } + + companion object : ValidatorFactory { + private val defaultExcludes by lazy { listOf("") } + + @JvmStatic + override fun create(options: List?): WithTotalDefaultRule { + return WithTotalDefaultRule(RuleSeverity.ERROR, options) + } + + @JvmStatic + override fun create(severity: RuleSeverity, options: List?): WithTotalDefaultRule { + return WithTotalDefaultRule(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 fab0dcaa..f0d649b1 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 @@ -528,4 +528,15 @@ class ValidatorRulesTest extends Specification implements ValidatorFixtures { then: result.validationResults.size() == 9 } + + def "with total default rule"() { + when: + def validators = Arrays.asList(new ResourcesValidator(Arrays.asList(WithTotalDefaultRule.create(emptyList())))) + def uri = uriFromClasspath("/with-total-default-rule.raml") + def result = new RamlModelBuilder(validators).buildApi(uri) + then: + result.validationResults.size() == 2 + result.validationResults[0].message == "Query parameter \"withTotal\" of method \"GET /invalid-resource-true\" must have a default value of \"false\", found \"true\"" + result.validationResults[1].message == "Query parameter \"withTotal\" of method \"GET /invalid-resource-no-default\" must have a default value of \"false\"" + } } diff --git a/ctp-validators/src/test/resources/with-total-default-rule.raml b/ctp-validators/src/test/resources/with-total-default-rule.raml new file mode 100644 index 00000000..0258b8a0 --- /dev/null +++ b/ctp-validators/src/test/resources/with-total-default-rule.raml @@ -0,0 +1,32 @@ +#%RAML 1.0 +title: with total default rule + +/valid-resource: + get: + queryParameters: + withTotal: + type: boolean + default: false + required: false + +/invalid-resource-true: + get: + queryParameters: + withTotal: + type: boolean + default: true + required: false + +/invalid-resource-no-default: + get: + queryParameters: + withTotal: + type: boolean + required: false + +/unrelated-resource: + get: + queryParameters: + limit: + type: number + default: 20 From 0ef3b26924c53fdbbae51da6b9e8955c7ab19fbf Mon Sep 17 00:00:00 2001 From: Mo Alras Date: Thu, 12 Mar 2026 10:41:53 +0100 Subject: [PATCH 2/2] Add test cases for shorthand and description-form parameters Ensure the rule handles all RAML parameter definition forms: shorthand, typed, and with descriptions (inline types). Co-Authored-By: Claude Opus 4.6 --- .../rmf/validators/ValidatorRulesTest.groovy | 4 +++- .../resources/with-total-default-rule.raml | 23 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) 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 f0d649b1..dc6d21f0 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 @@ -535,8 +535,10 @@ class ValidatorRulesTest extends Specification implements ValidatorFixtures { def uri = uriFromClasspath("/with-total-default-rule.raml") def result = new RamlModelBuilder(validators).buildApi(uri) then: - result.validationResults.size() == 2 + result.validationResults.size() == 4 result.validationResults[0].message == "Query parameter \"withTotal\" of method \"GET /invalid-resource-true\" must have a default value of \"false\", found \"true\"" result.validationResults[1].message == "Query parameter \"withTotal\" of method \"GET /invalid-resource-no-default\" must have a default value of \"false\"" + result.validationResults[2].message == "Query parameter \"withTotal\" of method \"GET /invalid-resource-shorthand\" must have a default value of \"false\"" + result.validationResults[3].message == "Query parameter \"withTotal\" of method \"GET /invalid-resource-desc-true\" must have a default value of \"false\", found \"true\"" } } diff --git a/ctp-validators/src/test/resources/with-total-default-rule.raml b/ctp-validators/src/test/resources/with-total-default-rule.raml index 0258b8a0..dfb95dd4 100644 --- a/ctp-validators/src/test/resources/with-total-default-rule.raml +++ b/ctp-validators/src/test/resources/with-total-default-rule.raml @@ -24,6 +24,29 @@ title: with total default rule type: boolean required: false +/invalid-resource-shorthand: + get: + queryParameters: + withTotal: boolean + +/invalid-resource-desc-true: + get: + queryParameters: + withTotal: + description: Include total count in response + type: boolean + default: true + required: false + +/valid-resource-desc: + get: + queryParameters: + withTotal: + description: Include total count in response + type: boolean + default: false + required: false + /unrelated-resource: get: queryParameters: