Consolidate duplicated Grails version comparison logic (VersionComparator vs GrailsVersion/Snapshot)
- Dominant language
- Groovy
- Stars
- 2.9k
- Forks
- 975
- Avg merge
- 1d 22h
- Merged PRs (30d)
- 92
Description
## Background
There are currently three independent implementations of Grails version parsing/comparison in the repository:
| Class | Module | Notes |
|-------|--------|-------|
| `grails.plugins.VersionComparator` | `grails-bootstrap` | Lenient `Comparator`; accepts 2-part versions, wildcards (`*`) and arbitrary plugin-declared ranges. Used by the plugin compatibility check and URL mapping version precedence. |
| `org.grails.datastore.mapping.core.grailsversion.GrailsVersion` / `Snapshot` | `grails-datastore-core` | Strict; requires 3-4 part versions and validates qualifiers against `^(M\|RC\|Final)\d*$`. The canonical milestone/RC/snapshot ordering. |
| `grails.init.GrailsVersion` | `grails-wrapper` | Another full parser/comparator used by the wrapper. |
The milestone/release-candidate ordering logic (`M < RC < SNAPSHOT < final`) is therefore implemented more than once. This was highlighted while fixing #14058, where `VersionComparator` had to re-implement the same ordering that `GrailsVersion`/`Snapshot` already encode, because:
1. `grails-bootstrap` is a deliberately dependency-light, low-level module and cannot reasonably depend on `grails-datastore-core` (GORM core) just to reuse `GrailsVersion`.
2. `GrailsVersion` is stricter than `VersionComparator` needs to be (it throws `IllegalArgumentException` on 2-part versions such as `4.0`, `1.0`, `2.5` that `VersionComparator` and its callers rely on).
## Proposal
Extract the shared version-ordering core (numeric component comparison + qualifier tier ordering) into a single low-level location that every module can reuse - for example a class in `grails-common` (which `grails-datastore-core` already depends on, and which `grails-bootstrap` could depend on without a cycle).
Goals:
- One implementation of the milestone/RC/snapshot ordering, reused by `VersionComparator`, `GrailsVersion`/`Snapshot`, and ideally the wrapper's `GrailsVersion`.
- Preserve the lenient behaviour required by `VersionComparator` (2-part versions, `*` wildcard, unknown qualifiers) while keeping the strict validation `GrailsVersion` exposes to its own callers.
- No behaviour change for existing consumers; consolidate behind the existing public APIs.
## Notes
- This is a refactor/maintenance task, not a bug. The behaviour itself is fixed in #14058.
- Because it would move/extract public classes and touch the GORM datastore module, it needs the cross-module review that dependency/structure changes require (see `CONTRIBUTING.md`).
Contributor guide
Research direction
Start by reading the existing VersionComparator, org.grails.datastore.mapping.core.grailsversion.GrailsVersion/Snapshot, and grails.init.GrailsVersion implementations, then review CONTRIBUTING.md for cross-module dependency guidance. Compare their qualifier ordering, validation, and lenient cases before evaluating the proposed grails-common location. Done means the existing public APIs preserve their behavior while sharing one ordering implementation across the applicable modules.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- groovy
- Domain
- backend
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100