Skip to content

Commit

Permalink
feat: Add must-understand CacheControl header directive support.
Browse files Browse the repository at this point in the history
  • Loading branch information
nstdio committed Apr 8, 2022
1 parent b7ae45a commit f0582bf
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,6 @@ import kotlin.String
import kotlin.Suppress
import kotlin.to

/*
* Copyright (C) 2022 Edgar Asatryan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

plugins {
`java-library`
idea
Expand Down Expand Up @@ -88,6 +72,7 @@ tasks.withType<Test> {

val junitVersion = "5.8.2"
val assertJVersion = "3.22.0"
val kotestAssertionsVersion = "5.2.2"
val mockitoVersion = "4.4.0"
val jsonPathAssertVersion = "2.7.0"
val slf4jVersion = "1.7.36"
Expand All @@ -114,6 +99,8 @@ dependencies {

/** AssertJ & Friends */
testImplementation("org.assertj:assertj-core:$assertJVersion")
testImplementation("io.kotest:kotest-assertions-core:$kotestAssertionsVersion")
testImplementation("io.kotest:kotest-property:$kotestAssertionsVersion")
testImplementation("com.jayway.jsonpath:json-path-assert:$jsonPathAssertVersion")

/** Jupiter */
Expand Down
30 changes: 22 additions & 8 deletions src/main/java/io/github/nstdio/http/ext/CacheControl.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@ public class CacheControl {
private static final Pattern VALUE_DIRECTIVES_PATTERN = Pattern.compile("(max-age|max-stale|min-fresh|stale-if-error|stale-while-revalidate)=(?:([0-9]+)|\"([0-9]+)\")");

//@formatter:off
private static final int NO_CACHE = 1 << 1;
private static final int NO_STORE = 1 << 2;
private static final int MUST_REVALIDATE = 1 << 3;
private static final int NO_TRANSFORM = 1 << 4;
private static final int IMMUTABLE = 1 << 5;
private static final int ONLY_IF_CACHED = 1 << 6;
private static final int NO_CACHE = 1 << 1;
private static final int NO_STORE = 1 << 2;
private static final int MUST_REVALIDATE = 1 << 3;
private static final int NO_TRANSFORM = 1 << 4;
private static final int IMMUTABLE = 1 << 5;
private static final int ONLY_IF_CACHED = 1 << 6;
private static final int MUST_UNDERSTAND = 1 << 7;
//@formatter:on

public static final CacheControl FORCE_CACHE = builder().onlyIfCached().maxAge(Long.MAX_VALUE).build();
Expand Down Expand Up @@ -91,6 +92,9 @@ public static CacheControl parse(String value) {
case "only-if-cached":
builder.onlyIfCached();
break;
case "must-understand":
builder.mustUnderstand();
break;
default: {
parseValue(builder, s);
break;
Expand Down Expand Up @@ -161,7 +165,7 @@ public boolean noCache() {
}

public boolean noStore() {
return isSet(flags, NO_STORE);
return !mustUnderstand() && isSet(flags, NO_STORE);
}

public boolean mustRevalidate() {
Expand Down Expand Up @@ -220,16 +224,21 @@ public boolean onlyIfCached() {
return isSet(flags, ONLY_IF_CACHED);
}

public boolean mustUnderstand() {
return isSet(flags, MUST_UNDERSTAND);
}

@Override
public String toString() {
var sb = new StringBuilder();
if (noCache()) sb.append("no-cache");
if (noStore()) appendComma(sb).append("no-store");
if (isSet(flags, NO_STORE)) appendComma(sb).append("no-store");
if (mustRevalidate()) appendComma(sb).append("must-revalidate");

if (noTransform()) appendComma(sb).append("no-transform");
if (immutable()) appendComma(sb).append("immutable");
if (onlyIfCached()) appendComma(sb).append("only-if-cached");
if (mustUnderstand()) appendComma(sb).append("must-understand");

if (maxAge > -1) appendComma(sb).append("max-age=").append(maxAge);
if (maxStale > -1) appendComma(sb).append("max-stale=").append(maxStale);
Expand Down Expand Up @@ -312,6 +321,11 @@ public CacheControlBuilder onlyIfCached() {
return this;
}

public CacheControlBuilder mustUnderstand() {
flags = set(flags, MUST_UNDERSTAND);
return this;
}

public CacheControl build() {
return new CacheControl(flags, maxAge, maxStale, minFresh, staleIfError, staleWhileRevalidate);
}
Expand Down
145 changes: 80 additions & 65 deletions src/test/kotlin/io/github/nstdio/http/ext/CacheControlTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,83 +17,98 @@

package io.github.nstdio.http.ext

import org.assertj.core.api.Assertions
import io.kotest.matchers.booleans.shouldBeFalse
import io.kotest.matchers.booleans.shouldBeTrue
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldBeEmpty
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
import java.net.http.HttpHeaders
import java.util.Map

internal class CacheControlTest {
@ParameterizedTest
@ValueSource(strings = ["max-age=92233720368547758070,max-stale=92233720368547758070,min-fresh=92233720368547758070", "max-age= ,max-stale=,min-fresh=abc"])
fun shouldNotFailWhenLongOverflow(value: String) {
//given
val httpHeaders =
HttpHeaders.of(Map.of(Headers.HEADER_CACHE_CONTROL, listOf(value))) { _: String?, _: String? -> true }
@ParameterizedTest
@ValueSource(strings = ["max-age=92233720368547758070,max-stale=92233720368547758070,min-fresh=92233720368547758070", "max-age= ,max-stale=,min-fresh=abc"])
fun shouldNotFailWhenLongOverflow(value: String) {
//given
val httpHeaders =
HttpHeaders.of(Map.of(Headers.HEADER_CACHE_CONTROL, listOf(value))) { _: String?, _: String? -> true }

//when
val actual = CacheControl.of(httpHeaders)
//when
val actual = CacheControl.of(httpHeaders)

//then
Assertions.assertThat(actual).hasToString("")
}
//then
actual.toString().shouldBeEmpty()
}

@ParameterizedTest
@ValueSource(strings = ["max-age=32,max-stale=64,min-fresh=128,stale-if-error=256,stale-while-revalidate=512", "max-age=\"32\" ,max-stale=\"64\",min-fresh=\"128\",stale-if-error=\"256\",stale-while-revalidate=\"512\""])
fun shouldParseValues(value: String) {
//given
val httpHeaders =
HttpHeaders.of(Map.of(Headers.HEADER_CACHE_CONTROL, listOf(value)), ) { _: String?, _: String? -> true }
@ParameterizedTest
@ValueSource(strings = ["max-age=32,max-stale=64,min-fresh=128,stale-if-error=256,stale-while-revalidate=512", "max-age=\"32\" ,max-stale=\"64\",min-fresh=\"128\",stale-if-error=\"256\",stale-while-revalidate=\"512\""])
fun shouldParseValues(value: String) {
//given
val httpHeaders =
HttpHeaders.of(Map.of(Headers.HEADER_CACHE_CONTROL, listOf(value))) { _: String?, _: String? -> true }

//when
val actual = CacheControl.of(httpHeaders)
//when
val actual = CacheControl.of(httpHeaders)

//then
Assertions.assertThat(actual.maxAge()).isEqualTo(32)
Assertions.assertThat(actual.maxStale()).isEqualTo(64)
Assertions.assertThat(actual.minFresh()).isEqualTo(128)
Assertions.assertThat(actual.staleIfError()).isEqualTo(256)
Assertions.assertThat(actual.staleWhileRevalidate()).isEqualTo(512)
Assertions.assertThat(actual)
.hasToString("max-age=32, max-stale=64, min-fresh=128, stale-if-error=256, stale-while-revalidate=512")
}
//then
assertThat(actual.maxAge()).isEqualTo(32)
assertThat(actual.maxStale()).isEqualTo(64)
assertThat(actual.minFresh()).isEqualTo(128)
assertThat(actual.staleIfError()).isEqualTo(256)
assertThat(actual.staleWhileRevalidate()).isEqualTo(512)
assertThat(actual)
.hasToString("max-age=32, max-stale=64, min-fresh=128, stale-if-error=256, stale-while-revalidate=512")
}

@Test
fun shouldParseAndRoundRobin() {
//given
val minFresh = 5
val maxStale = 6
val maxAge = 7
val staleIfError = 8
val staleWhileRevalidate = 9
val cc = CacheControl
.builder()
.minFresh(minFresh.toLong())
.maxStale(maxStale.toLong())
.maxAge(maxAge.toLong())
.staleIfError(staleIfError.toLong())
.staleWhileRevalidate(staleWhileRevalidate.toLong())
.noCache()
.noTransform()
.onlyIfCached()
.noStore()
.mustRevalidate()
.immutable()
.build()
val expected = String.format(
"no-cache, no-store, must-revalidate, no-transform, immutable, only-if-cached, max-age=%d, max-stale=%d, min-fresh=%d, stale-if-error=%d, stale-while-revalidate=%d",
maxAge,
maxStale,
minFresh,
staleIfError,
staleWhileRevalidate
)
@Test
fun shouldParseAndRoundRobin() {
//given
val minFresh = 5
val maxStale = 6
val maxAge = 7
val staleIfError = 8
val staleWhileRevalidate = 9
val cc = CacheControl
.builder()
.minFresh(minFresh.toLong())
.maxStale(maxStale.toLong())
.maxAge(maxAge.toLong())
.staleIfError(staleIfError.toLong())
.staleWhileRevalidate(staleWhileRevalidate.toLong())
.noCache()
.noTransform()
.onlyIfCached()
.noStore()
.mustUnderstand()
.mustRevalidate()
.immutable()
.build()
val expected = """
no-cache, no-store, must-revalidate, no-transform, immutable, only-if-cached, must-understand,
max-age=$maxAge, max-stale=$maxStale, min-fresh=$minFresh, stale-if-error=$staleIfError, stale-while-revalidate=$staleWhileRevalidate
""".trimIndent().replace("\n", "")

//when + then
Assertions.assertThat(cc).hasToString(expected)
Assertions.assertThat(CacheControl.parse(expected))
.usingRecursiveComparison()
.isEqualTo(cc)
}
//when + then
cc.toString().shouldBe(expected)
assertThat(CacheControl.parse(expected))
.usingRecursiveComparison()
.isEqualTo(cc)
}

@Test
fun `Should support must-understand directive`() {
//given
val value = "must-understand, no-store, max-age=86400"

//when
val actual = CacheControl.parse(value)

//then
actual.noStore().shouldBeFalse()
actual.mustUnderstand().shouldBeTrue()
actual.maxAge().shouldBe(86400)
}
}

0 comments on commit f0582bf

Please sign in to comment.