package dev.coph.nextusweb.server.ratelimit; import java.util.*; /** * Immutable mapping from request paths to the {@link Rule rate-limit rules} that apply to them. * *

Three kinds of rules can be configured, resolved with the following precedence by * {@link #rulesFor(String)}:

*
    *
  1. an optional global rule that applies to every request;
  2. *
  3. exact-path rules matched by exact path equality;
  4. *
  5. prefix rules matched by path prefix, evaluated longest-prefix-first.
  6. *
* *

A request is subject to the global rule (if any) plus the single most specific path rule * that matches. Instances are built through the nested {@link Builder}.

*/ public final class RateLimitConfig { /** * Rule applied to every request, or {@code null} if no global rule is configured. */ private final Rule globalRule; /** * Rules matched by exact path equality, keyed by path. */ private final Map exactPathRules; /** * Prefix rules, pre-sorted longest-prefix-first so the most specific match wins. */ private final List prefixRules; /** * Every distinct limiter referenced by any rule, by identity; used for periodic cleanup. */ private final Set allLimiters; /** * Builds an immutable configuration from a {@link Builder}, copying the exact-path rules * and sorting the prefix rules by descending prefix length. * * @param b the builder carrying the configured rules */ private RateLimitConfig(Builder b) { this.globalRule = b.globalRule; this.exactPathRules = Map.copyOf(b.exactPathRules); this.prefixRules = b.prefixRules.stream() .sorted((a, c) -> Integer.compare(c.prefix.length(), a.prefix.length())) .toList(); Set limiters = Collections.newSetFromMap(new IdentityHashMap<>()); if (globalRule != null) limiters.add(globalRule.limiter()); for (Rule r : exactPathRules.values()) limiters.add(r.limiter()); for (PrefixRule pr : prefixRules) limiters.add(pr.rule.limiter()); this.allLimiters = Collections.unmodifiableSet(limiters); } /** * Creates a new, empty {@link Builder}. * * @return a fresh builder */ public static Builder builder() { return new Builder(); } /** * Returns the ordered list of rules that apply to the given path. * *

The list contains the global rule first (if configured) followed by at most one * path-specific rule: the exact-path rule if one matches, otherwise the longest matching * prefix rule. The returned list may be empty if no rule applies.

* * @param path the request path * @return the applicable rules, in evaluation order */ public List rulesFor(String path) { List rules = new ArrayList<>(2); if (globalRule != null) rules.add(globalRule); Rule exact = exactPathRules.get(path); if (exact != null) { rules.add(exact); return rules; } for (PrefixRule pr : prefixRules) { if (path.startsWith(pr.prefix)) { rules.add(pr.rule); return rules; } } return rules; } /** * Returns every distinct limiter referenced by this configuration, for periodic state * eviction by {@link RateLimitGate}. * * @return the immutable set of distinct limiters (de-duplicated by identity) */ public Set allLimiters() { return allLimiters; } /** * A single rate-limit rule: a limiter, the key resolver feeding it, and a name used to * namespace keys and aid diagnostics. * * @param limiter the limiter that enforces the quota * @param keyResolver resolves the per-request key the limiter buckets on * @param name a human-readable label (e.g. {@code "global"} or a path/prefix) */ public record Rule(RateLimiter limiter, KeyResolver keyResolver, String name) { } /** * Internal pairing of a path prefix with the rule that applies to paths starting with it. * * @param prefix the path prefix * @param rule the rule to apply for matching paths */ private record PrefixRule(String prefix, Rule rule) { } /** * Fluent builder for {@link RateLimitConfig}. */ public static final class Builder { /** * Accumulated exact-path rules, keyed by path. */ private final Map exactPathRules = new HashMap<>(); /** * Accumulated prefix rules. */ private final List prefixRules = new ArrayList<>(); /** * The global rule, if configured. */ private Rule globalRule; /** * Creates a builder with no rules configured. Obtain instances via * {@link RateLimitConfig#builder()}. */ public Builder() { } /** * Sets the global rule applied to every request. * * @param limiter the limiter enforcing the global quota * @param keys the key resolver for the global rule * @return this builder, for fluent chaining */ public Builder global(RateLimiter limiter, KeyResolver keys) { this.globalRule = new Rule(limiter, keys, "global"); return this; } /** * Adds a rule that applies only to requests whose path equals {@code path} exactly. * * @param path the exact request path * @param limiter the limiter enforcing the quota * @param keys the key resolver for this rule * @return this builder, for fluent chaining */ public Builder forPath(String path, RateLimiter limiter, KeyResolver keys) { exactPathRules.put(path, new Rule(limiter, keys, path)); return this; } /** * Adds a rule that applies to requests whose path starts with {@code prefix}. When * several prefixes match, the longest one wins. * * @param prefix the path prefix * @param limiter the limiter enforcing the quota * @param keys the key resolver for this rule * @return this builder, for fluent chaining */ public Builder forPrefix(String prefix, RateLimiter limiter, KeyResolver keys) { prefixRules.add(new PrefixRule(prefix, new Rule(limiter, keys, prefix + "*"))); return this; } /** * Builds the immutable {@link RateLimitConfig}. * * @return the configured instance */ public RateLimitConfig build() { return new RateLimitConfig(this); } } }