diff --git a/README.md b/README.md index 2af0f8a..2fb7676 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ library for introducing disruptions to your code to replicate an unstable live e - **core:** core disruptor API - ~~**spring:** spring integration~~ -- ~~**feign:** feign integration~~ +- **openfeign:** feign integration ## Example @@ -40,4 +40,20 @@ final Disruptor disruptor = Disruptor.builder() disruptor.disruptWithoutResult("test", () -> { System.out.println("test"); }); +``` + +### OpenFeign + +```java +final DisruptorCapability capability = DisruptorCapability.of( + disruptor, + "group" +); +Feign.builder()/*...*/.addCapability(capability)/*...*/; + +// Or, if using spring-cloud-openfeign: +@Bean +Capability disruptorCapability() { + return DisruptorCapability(disruptor, "group"); +} ``` \ No newline at end of file diff --git a/TODO.md b/TODO.md index e1445f0..8b15c27 100644 --- a/TODO.md +++ b/TODO.md @@ -3,4 +3,4 @@ 1. [x] Create core API 2. [x] Tests :) 3. [ ] Create Spring (AOP) integration -4. [ ] Create Feign integration \ No newline at end of file +4. [x] Create Feign integration \ No newline at end of file diff --git a/core/src/main/java/org/incendo/disruptor/DisruptionConfig.java b/core/src/main/java/org/incendo/disruptor/DisruptionConfig.java index fc3d198..b348e2a 100644 --- a/core/src/main/java/org/incendo/disruptor/DisruptionConfig.java +++ b/core/src/main/java/org/incendo/disruptor/DisruptionConfig.java @@ -23,12 +23,7 @@ // package org.incendo.disruptor; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; -import java.util.Objects; -import java.util.function.Function; import org.apiguardian.api.API; import org.incendo.disruptor.disruption.Disruption; import org.incendo.disruptor.trigger.DisruptionTrigger; @@ -41,8 +36,8 @@ public interface DisruptionConfig { * * @return the builder */ - static Builder builder() { - return new Builder(); + static DisruptionConfigBuilder builder() { + return new DisruptionConfigBuilder(); } /** @@ -65,96 +60,4 @@ static Builder builder() { * @return the mode */ DisruptionMode mode(); - - @API(status = API.Status.STABLE, since = "1.0.0") - final class Builder { - - private final List disruptions = new ArrayList<>(); - private DisruptionTrigger trigger = DisruptionTrigger.never(); - private DisruptionMode mode = DisruptionMode.BEFORE; - - private Builder() { - } - - /** - * Sets the trigger. - * - * @param trigger new trigger - * @return {@code this} - */ - public Builder trigger(final DisruptionTrigger trigger) { - this.trigger = Objects.requireNonNull(trigger, "trigger"); - return this; - } - - /** - * Adds the given {@code disruptions}. - * - * @param disruptions disruptions to add - * @return {@code this} - */ - public Builder disruptions(final Disruption... disruptions) { - Objects.requireNonNull(disruptions, "disruptions"); - for (final Disruption disruption : disruptions) { - Objects.requireNonNull(disruption, "disruption"); - } - this.disruptions.addAll(Arrays.asList(disruptions)); - return this; - } - - /** - * Adds the given {@code disruptions}. - * - * @param disruptions disruptions to add - * @return {@code this} - */ - public Builder disruptions(final List disruptions) { - Objects.requireNonNull(disruptions, "disruptions"); - for (final Disruption disruption : disruptions) { - Objects.requireNonNull(disruption, "disruption"); - } - this.disruptions.addAll(disruptions); - return this; - } - - /** - * Adds a {@link Disruption#delaying(Duration)} disruption. - * - * @param duration duration to delay for - * @return {@code this} - */ - public Builder delay(final Duration duration) { - return this.disruptions(Disruption.delaying(duration)); - } - - /** - * Adds a {@link Disruption#throwing(Function)} disruption. - * - * @param generator throwable generator - * @return {@code this} - */ - public Builder throwException(final Function generator) { - return this.disruptions(Disruption.throwing(generator)); - } - - /** - * Sets the disruption mode to the given {@code mode}. - * - * @param mode new mode - * @return {@code this} - */ - public Builder mode(final DisruptionMode mode) { - this.mode = Objects.requireNonNull(mode, "mode"); - return this; - } - - /** - * Build a new {@link DisruptionConfig} instance using {@code this} builder. - * - * @return the config instance - */ - public DisruptionConfig build() { - return new DisruptionConfigImpl(this.trigger, List.copyOf(this.disruptions), this.mode); - } - } } diff --git a/core/src/main/java/org/incendo/disruptor/DisruptionConfigBuilder.java b/core/src/main/java/org/incendo/disruptor/DisruptionConfigBuilder.java new file mode 100644 index 0000000..3d58aa2 --- /dev/null +++ b/core/src/main/java/org/incendo/disruptor/DisruptionConfigBuilder.java @@ -0,0 +1,126 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.disruptor; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import org.apiguardian.api.API; +import org.incendo.disruptor.disruption.Disruption; +import org.incendo.disruptor.trigger.DisruptionTrigger; + +@API(status = API.Status.STABLE, since = "1.0.0") +public final class DisruptionConfigBuilder { + + private final List disruptions = new ArrayList<>(); + private DisruptionTrigger trigger = DisruptionTrigger.never(); + private DisruptionMode mode = DisruptionMode.BEFORE; + + DisruptionConfigBuilder() { + } + + /** + * Sets the trigger. + * + * @param trigger new trigger + * @return {@code this} + */ + public DisruptionConfigBuilder trigger(final DisruptionTrigger trigger) { + this.trigger = Objects.requireNonNull(trigger, "trigger"); + return this; + } + + /** + * Adds the given {@code disruptions}. + * + * @param disruptions disruptions to add + * @return {@code this} + */ + public DisruptionConfigBuilder disruptions(final Disruption... disruptions) { + Objects.requireNonNull(disruptions, "disruptions"); + for (final Disruption disruption : disruptions) { + Objects.requireNonNull(disruption, "disruption"); + } + this.disruptions.addAll(Arrays.asList(disruptions)); + return this; + } + + /** + * Adds the given {@code disruptions}. + * + * @param disruptions disruptions to add + * @return {@code this} + */ + public DisruptionConfigBuilder disruptions(final List disruptions) { + Objects.requireNonNull(disruptions, "disruptions"); + for (final Disruption disruption : disruptions) { + Objects.requireNonNull(disruption, "disruption"); + } + this.disruptions.addAll(disruptions); + return this; + } + + /** + * Adds a {@link Disruption#delaying(Duration)} disruption. + * + * @param duration duration to delay for + * @return {@code this} + */ + public DisruptionConfigBuilder delay(final Duration duration) { + return this.disruptions(Disruption.delaying(duration)); + } + + /** + * Adds a {@link Disruption#throwing(Function)} disruption. + * + * @param generator throwable generator + * @return {@code this} + */ + public DisruptionConfigBuilder throwException(final Function generator) { + return this.disruptions(Disruption.throwing(generator)); + } + + /** + * Sets the disruption mode to the given {@code mode}. + * + * @param mode new mode + * @return {@code this} + */ + public DisruptionConfigBuilder mode(final DisruptionMode mode) { + this.mode = Objects.requireNonNull(mode, "mode"); + return this; + } + + /** + * Build a new {@link DisruptionConfig} instance using {@code this} builder. + * + * @return the config instance + */ + public DisruptionConfig build() { + return new DisruptionConfigImpl(this.trigger, List.copyOf(this.disruptions), this.mode); + } +} diff --git a/core/src/main/java/org/incendo/disruptor/Disruptor.java b/core/src/main/java/org/incendo/disruptor/Disruptor.java index 09a54a7..857dcac 100644 --- a/core/src/main/java/org/incendo/disruptor/Disruptor.java +++ b/core/src/main/java/org/incendo/disruptor/Disruptor.java @@ -23,11 +23,7 @@ // package org.incendo.disruptor; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; import java.util.Optional; -import java.util.function.Consumer; import java.util.function.Supplier; import org.apiguardian.api.API; @@ -47,8 +43,8 @@ public interface Disruptor { * * @return a mutable builder */ - static Builder builder() { - return new Builder(); + static DisruptorBuilder builder() { + return new DisruptorBuilder(); } /** @@ -104,6 +100,20 @@ default void disruptWithoutResult(final String group, Runnable runnable) { }); } + /** + * Triggers the disruptions for the given {@code group} and {@code mode}. + * + * @param group disruption group to trigger + * @param mode mode to trigger + */ + default void disrupt(final String group, final DisruptionMode mode) { + this.group(group).ifPresent(disruptorGroup -> this.triggerDisruptions( + DisruptorContext.of(group), + disruptorGroup, + mode + )); + } + private void triggerDisruptions( final DisruptorContext context, final DisruptorGroup group, @@ -117,68 +127,4 @@ private void triggerDisruptions( .forEach(disruption -> disruption.trigger(context)); } - /** - * Builder for {@link Disruptor} instances. The builder should be constructed using {@link #builder()}. - * - * @since 1.0.0 - */ - @API(status = API.Status.STABLE, since = "1.0.0") - final class Builder { - - private final Map groups = new HashMap<>(); - - private Builder() { - } - - /** - * Adds the given {@code group} with the given {@code name} to the disruptor instance. - * - * @param name group name - * @param group group - * @return {@code this} - */ - public Builder group( - final String name, - final DisruptorGroup group - ) { - Objects.requireNonNull(name, "name"); - Objects.requireNonNull(group, "group"); - - this.groups.put(name, group); - - return this; - } - - /** - * Adds the disruptor group with the given {@code name} to the disruptor instance, - * after letting the {@code decorator} decorate the group builder. - * - * @param name group name - * @param decorator group decorator - * @return {@code this} - */ - public Builder group( - final String name, - final Consumer decorator - ) { - Objects.requireNonNull(name, "name"); - Objects.requireNonNull(decorator, "decorator"); - - final DisruptorGroup.Builder builder = DisruptorGroup.builder(); - decorator.accept(builder); - - return this.group(name, builder.build()); - } - - /** - * Build a new {@link Disruptor} instance using {@code this} builder. - * - * @return the disruptor instance - */ - public Disruptor build() { - return new DisruptorImpl( - Map.copyOf(this.groups) - ); - } - } } diff --git a/core/src/main/java/org/incendo/disruptor/DisruptorBuilder.java b/core/src/main/java/org/incendo/disruptor/DisruptorBuilder.java new file mode 100644 index 0000000..7b45353 --- /dev/null +++ b/core/src/main/java/org/incendo/disruptor/DisruptorBuilder.java @@ -0,0 +1,95 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.disruptor; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; +import org.apiguardian.api.API; + +/** + * Builder for {@link Disruptor} instances. The builder should be constructed using {@link Disruptor#builder()}. + * + * @since 1.0.0 + */ +@API(status = API.Status.STABLE, since = "1.0.0") +public final class DisruptorBuilder { + + private final Map groups = new HashMap<>(); + + DisruptorBuilder() { + } + + /** + * Adds the given {@code group} with the given {@code name} to the disruptor instance. + * + * @param name group name + * @param group group + * @return {@code this} + */ + public DisruptorBuilder group( + final String name, + final DisruptorGroup group + ) { + Objects.requireNonNull(name, "name"); + Objects.requireNonNull(group, "group"); + + this.groups.put(name, group); + + return this; + } + + /** + * Adds the disruptor group with the given {@code name} to the disruptor instance, + * after letting the {@code decorator} decorate the group builder. + * + * @param name group name + * @param decorator group decorator + * @return {@code this} + */ + public DisruptorBuilder group( + final String name, + final Consumer decorator + ) { + Objects.requireNonNull(name, "name"); + Objects.requireNonNull(decorator, "decorator"); + + final DisruptorGroupBuilder builder = DisruptorGroup.builder(); + decorator.accept(builder); + + return this.group(name, builder.build()); + } + + /** + * Build a new {@link Disruptor} instance using {@code this} builder. + * + * @return the disruptor instance + */ + public Disruptor build() { + return new DisruptorImpl( + Map.copyOf(this.groups) + ); + } +} diff --git a/core/src/main/java/org/incendo/disruptor/DisruptorGroup.java b/core/src/main/java/org/incendo/disruptor/DisruptorGroup.java index 1badda4..58843d5 100644 --- a/core/src/main/java/org/incendo/disruptor/DisruptorGroup.java +++ b/core/src/main/java/org/incendo/disruptor/DisruptorGroup.java @@ -23,12 +23,8 @@ // package org.incendo.disruptor; -import java.util.ArrayList; import java.util.List; -import java.util.Objects; -import java.util.function.Consumer; import org.apiguardian.api.API; -import org.incendo.disruptor.trigger.DisruptionTrigger; /** * A disruptor configuration group. @@ -45,8 +41,8 @@ public interface DisruptorGroup { * * @return a mutable builder */ - static Builder builder() { - return new Builder(); + static DisruptorGroupBuilder builder() { + return new DisruptorGroupBuilder(); } /** @@ -55,71 +51,4 @@ static Builder builder() { * @return the configurations */ List configurations(); - - @API(status = API.Status.STABLE, since = "1.0.0") - final class Builder { - - private final List configurations = new ArrayList<>(); - - private Builder() { - } - - /** - * Adds the given {@code config} to the group. - * - * @param config the config - * @return {@code this} - */ - public Builder config(final DisruptionConfig config) { - Objects.requireNonNull(config, "config"); - this.configurations.add(config); - return this; - } - - /** - * Adds the given {@code config} to the group. - * - * @param config the config - * @return {@code this} - */ - public Builder config(final DisruptionConfig.Builder config) { - return this.config(config.build()); - } - - /** - * Adds a config to the group after letting the given {@code decorator} decorate the config builder. - * - * @param decorator the decorator - * @return {@code this} - */ - public Builder config(final Consumer decorator) { - Objects.requireNonNull(decorator, "config"); - final DisruptionConfig.Builder builder = DisruptionConfig.builder(); - decorator.accept(builder); - return this.config(builder); - } - - /** - * Adds a config to the group after letting the given {@code decorator} decorate the config builder. - * - * @param trigger the disruption trigger - * @param decorator the decorator - * @return {@code this} - */ - public Builder config(final DisruptionTrigger trigger, final Consumer decorator) { - Objects.requireNonNull(decorator, "config"); - final DisruptionConfig.Builder builder = DisruptionConfig.builder().trigger(trigger); - decorator.accept(builder); - return this.config(builder); - } - - /** - * Build a new {@link DisruptorGroup} instance using {@code this} builder. - * - * @return the disruptor group instance - */ - public DisruptorGroup build() { - return new DisruptorGroupImpl(List.copyOf(this.configurations)); - } - } } diff --git a/core/src/main/java/org/incendo/disruptor/DisruptorGroupBuilder.java b/core/src/main/java/org/incendo/disruptor/DisruptorGroupBuilder.java new file mode 100644 index 0000000..1770eba --- /dev/null +++ b/core/src/main/java/org/incendo/disruptor/DisruptorGroupBuilder.java @@ -0,0 +1,98 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.disruptor; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import org.apiguardian.api.API; +import org.incendo.disruptor.trigger.DisruptionTrigger; + +@API(status = API.Status.STABLE, since = "1.0.0") +public final class DisruptorGroupBuilder { + + private final List configurations = new ArrayList<>(); + + DisruptorGroupBuilder() { + } + + /** + * Adds the given {@code config} to the group. + * + * @param config the config + * @return {@code this} + */ + public DisruptorGroupBuilder config(final DisruptionConfig config) { + Objects.requireNonNull(config, "config"); + this.configurations.add(config); + return this; + } + + /** + * Adds the given {@code config} to the group. + * + * @param config the config + * @return {@code this} + */ + public DisruptorGroupBuilder config(final DisruptionConfigBuilder config) { + return this.config(config.build()); + } + + /** + * Adds a config to the group after letting the given {@code decorator} decorate the config builder. + * + * @param decorator the decorator + * @return {@code this} + */ + public DisruptorGroupBuilder config(final Consumer decorator) { + Objects.requireNonNull(decorator, "config"); + final DisruptionConfigBuilder builder = DisruptionConfig.builder(); + decorator.accept(builder); + return this.config(builder); + } + + /** + * Adds a config to the group after letting the given {@code decorator} decorate the config builder. + * + * @param trigger the disruption trigger + * @param decorator the decorator + * @return {@code this} + */ + public DisruptorGroupBuilder config(final DisruptionTrigger trigger, final Consumer decorator) { + Objects.requireNonNull(decorator, "config"); + final DisruptionConfigBuilder builder = DisruptionConfig.builder().trigger(trigger); + decorator.accept(builder); + return this.config(builder); + } + + /** + * Build a new {@link DisruptorGroup} instance using {@code this} builder. + * + * @return the disruptor group instance + */ + public DisruptorGroup build() { + return new DisruptorGroupImpl(List.copyOf(this.configurations)); + } +} diff --git a/gradle.properties b/gradle.properties index 9a40796..f4aaa56 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ -group="org.incendo" -version="0.0.1-SNAPSHOT" -description="Disruptor" +group=org.incendo +version=0.0.1-SNAPSHOT +description=Disruptor org.gradle.configureondemand=true org.gradle.caching=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6b88785..e7f5a96 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,10 +16,12 @@ checkstyle = "10.18.2" slf4j = "2.0.16" jspecify = "1.0.0" apiguardian = "1.1.2" +feign = "13.5" # test truth = "1.4.4" junit = "5.7.1" +wiremock = "3.9.1" [libraries] # plugins @@ -35,7 +37,12 @@ slf4j = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" } jspecify = { group = "org.jspecify", name = "jspecify", version.ref = "jspecify" } apiguardian = { group = "org.apiguardian", name = "apiguardian-api", version.ref = "apiguardian" } +# feign +feign-core = { group = "io.github.openfeign", name = "feign-core", version.ref = "feign" } +feign-java11 = { group = "io.github.openfeign", name = "feign-java11", version.ref = "feign" } + # test truth = { group = "com.google.truth", name = "truth", version.ref = "truth" } junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junit" } -junit-platform = { group = "org.junit.platform", name = "junit-platform-launcher" } \ No newline at end of file +junit-platform = { group = "org.junit.platform", name = "junit-platform-launcher" } +wiremock = { group = "org.wiremock", name = "wiremock", version.ref = "wiremock" } \ No newline at end of file diff --git a/openfeign/build.gradle.kts b/openfeign/build.gradle.kts new file mode 100644 index 0000000..fc77347 --- /dev/null +++ b/openfeign/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + id("disruptor.base-conventions") + id("disruptor.publishing-conventions") +} + +dependencies { + api(projects.disruptor.core) + api(libs.feign.core) + + testImplementation(libs.wiremock) + testImplementation(libs.feign.java11) + testImplementation(libs.junit.jupiter) + testRuntimeOnly(libs.junit.platform) +} + +tasks.named("test") { + useJUnitPlatform() +} diff --git a/openfeign/src/main/java/org/incendo/disruptor/openfeign/DisruptorCapability.java b/openfeign/src/main/java/org/incendo/disruptor/openfeign/DisruptorCapability.java new file mode 100644 index 0000000..cb69127 --- /dev/null +++ b/openfeign/src/main/java/org/incendo/disruptor/openfeign/DisruptorCapability.java @@ -0,0 +1,93 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.disruptor.openfeign; + +import feign.Capability; +import feign.Client; +import feign.Request; +import feign.Response; +import java.io.IOException; +import java.util.Objects; +import java.util.function.Supplier; +import org.apiguardian.api.API; +import org.incendo.disruptor.DisruptionMode; +import org.incendo.disruptor.Disruptor; + +/** + * Feign capability which runs {@link Disruptor#disrupt(String, Supplier)} before and after the Feign client has + * executed a request. + * + *

This should be registered using {@link feign.Feign.Builder#addCapability(Capability)}, or + *

{@code
+ * @Bean
+ * Capability disruptorCapability() {
+ *   return DisruptorCapability.of(disruptor, group);
+ * }}
when using spring-cloud-openfeign. + * + * @since 1.0.0 + */ +@API(status = API.Status.STABLE, since = "1.0.0") +public final class DisruptorCapability implements Capability { + + /** + * Creates a new {@link DisruptorCapability}. + * + * @param disruptor disruptor instance + * @param group disruptor group to use for the Feign clients + * @return the capability + */ + public static DisruptorCapability of(final Disruptor disruptor, final String group) { + return new DisruptorCapability(disruptor, group); + } + + private final Disruptor disruptor; + private final String group; + + private DisruptorCapability(final Disruptor disruptor, final String group) { + this.disruptor = Objects.requireNonNull(disruptor, "disruptor"); + this.group = Objects.requireNonNull(group, "group"); + } + + @Override + public Client enrich(final Client client) { + return new DisruptorClient(client); + } + + private final class DisruptorClient implements Client { + + private final Client client; + + private DisruptorClient(final Client client) { + this.client = Objects.requireNonNull(client, "client"); + } + + @Override + public Response execute(final Request request, final Request.Options options) throws IOException { + DisruptorCapability.this.disruptor.disrupt(DisruptorCapability.this.group, DisruptionMode.BEFORE); + final Response result = this.client.execute(request, options); + DisruptorCapability.this.disruptor.disrupt(DisruptorCapability.this.group, DisruptionMode.AFTER); + return result; + } + } +} diff --git a/openfeign/src/main/java/org/incendo/disruptor/openfeign/package-info.java b/openfeign/src/main/java/org/incendo/disruptor/openfeign/package-info.java new file mode 100644 index 0000000..3dec4ff --- /dev/null +++ b/openfeign/src/main/java/org/incendo/disruptor/openfeign/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.incendo.disruptor.openfeign; + +import org.jspecify.annotations.NullMarked; diff --git a/openfeign/src/test/java/org/incendo/disruptor/openfeign/DisruptorCapabilityTest.java b/openfeign/src/test/java/org/incendo/disruptor/openfeign/DisruptorCapabilityTest.java new file mode 100644 index 0000000..9df0076 --- /dev/null +++ b/openfeign/src/test/java/org/incendo/disruptor/openfeign/DisruptorCapabilityTest.java @@ -0,0 +1,96 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.disruptor.openfeign; + +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import feign.Feign; +import feign.RequestLine; +import org.incendo.disruptor.DisruptionMode; +import org.incendo.disruptor.Disruptor; +import org.incendo.disruptor.DisruptorGroup; +import org.incendo.disruptor.trigger.DisruptionTrigger; +import org.junit.jupiter.api.Test; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@WireMockTest +class DisruptorCapabilityTest { + + @Test + void testBefore() { + // Arrange + final Disruptor disruptor = Disruptor.builder() + .group( + "feign", + DisruptorGroup.builder().config(config -> config.mode(DisruptionMode.BEFORE) + .trigger(DisruptionTrigger.random(1.0f)) + .throwException(ctx -> new RuntimeException("test")) + ).build() + ).build(); + final TestTarget client = Feign.builder() + .addCapability(DisruptorCapability.of(disruptor, "feign")) + .target(TestTarget.class, "http://localhost"); + + // Act & Assert + final RuntimeException exception = assertThrows( + RuntimeException.class, + client::get + ); + assertThat(exception).hasMessageThat().isEqualTo("test"); + } + + @Test + void testAfter(final WireMockRuntimeInfo wireMockRuntimeInfo) { + // Arrange + WireMock.stubFor(WireMock.get("/").willReturn(WireMock.jsonResponse("\"yay\"", 200))); + + final Disruptor disruptor = Disruptor.builder() + .group( + "feign", + DisruptorGroup.builder().config(config -> config.mode(DisruptionMode.AFTER) + .trigger(DisruptionTrigger.random(1.0f)) + .throwException(ctx -> new RuntimeException("test")) + ).build() + ).build(); + final TestTarget client = Feign.builder() + .addCapability(DisruptorCapability.of(disruptor, "feign")) + .target(TestTarget.class, wireMockRuntimeInfo.getHttpBaseUrl()); + + // Act & Assert + final RuntimeException exception = assertThrows( + RuntimeException.class, + client::get + ); + assertThat(exception).hasMessageThat().isEqualTo("test"); + } + + interface TestTarget { + + @RequestLine("GET /") + String get(); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 68ae74e..93188b6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -30,4 +30,4 @@ dependencyResolutionManagement { rootProject.name = "disruptor" include(":core") -include(":spring") \ No newline at end of file +include(":openfeign") \ No newline at end of file