Files
Nextus-Web/src/main/java/dev/coph/nextusweb/server/ratelimit/RateLimitConfig.java
T
CodingPhoenixx a7b65c031d
Auto Publish on Version Change / check-and-publish (push) Successful in 14s
Run Tests on Push and Pull Request / run-tests (push) Successful in 18s
Add constructors and Javadoc comments to improve clarity and completeness across server components, including WebSocket and routing classes.
2026-05-29 09:00:31 +02:00

169 lines
6.0 KiB
Java

package dev.coph.nextusweb.server.ratelimit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 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;
/**
* 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();
}
/**
* 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;
}
/**
* 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);
}
}
}