Files
Nextus-Web/src/main/java/dev/coph/nextusweb/server/ratelimit/RateLimitConfig.java
T
CodingPhoenix 893bb0b7bd
CI - Test, Publish and Release / run-tests (push) Failing after 15s
CI - Test, Publish and Release / create-release (push) Has been skipped
CI - Test, Publish and Release / check-and-publish (push) Has been skipped
Reformat code comments for consistency and clarity across all classes
2026-06-15 07:27:07 +02:00

198 lines
6.8 KiB
Java

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.
*
* <p>Three kinds of rules can be configured, resolved with the following precedence by
* {@link #rulesFor(String)}:</p>
* <ol>
* <li>an optional <strong>global</strong> rule that applies to every request;</li>
* <li><strong>exact-path</strong> rules matched by exact path equality;</li>
* <li><strong>prefix</strong> rules matched by path prefix, evaluated longest-prefix-first.</li>
* </ol>
*
* <p>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}.</p>
*/
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<String, Rule> exactPathRules;
/**
* Prefix rules, pre-sorted longest-prefix-first so the most specific match wins.
*/
private final List<PrefixRule> prefixRules;
/**
* Every distinct limiter referenced by any rule, by identity; used for periodic cleanup.
*/
private final Set<RateLimiter> 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<RateLimiter> 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.
*
* <p>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.</p>
*
* @param path the request path
* @return the applicable rules, in evaluation order
*/
public List<Rule> rulesFor(String path) {
List<Rule> 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<RateLimiter> 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<String, Rule> exactPathRules = new HashMap<>();
/**
* Accumulated prefix rules.
*/
private final List<PrefixRule> 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);
}
}
}