diff --git a/.gitignore b/.gitignore index 11756d0..dc57c96 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Gradle .gradle build/ +out/ # Ignore Gradle GUI config gradle-app.setting diff --git a/README.md b/README.md index 66a75e6..e42d99f 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ Take a look, play and have fun with it! `./gradlew assemble --warning-mode all` 2. Run the tests and plugins verification tasks: `./gradlew check --warning-mode all` + 3. Execute the main application entrypoint: + `./gradlew run --warning-mode all` 4. Start developing! ## 🤔 How to update dependencies diff --git a/build.gradle b/build.gradle index a530d9f..1a08332 100644 --- a/build.gradle +++ b/build.gradle @@ -1,15 +1,22 @@ apply plugin: 'java' +apply plugin: 'application' sourceCompatibility = 1.11 targetCompatibility = 1.11 repositories { mavenCentral() + jcenter() } dependencies { - testCompile('org.junit.jupiter:junit-jupiter-api:5.3.2') - testRuntime('org.junit.jupiter:junit-jupiter-engine:5.3.2') + // Prod + implementation 'io.projectreactor:reactor-bus:2.0.8.RELEASE' + + // Test + testCompile "org.mockito:mockito-core:2.+" + testCompile 'org.junit.jupiter:junit-jupiter-api:5.+' + testRuntime 'org.junit.jupiter:junit-jupiter-engine:5.+' } test { @@ -22,4 +29,8 @@ test { reports { html.enabled = true } -} \ No newline at end of file +} + +application { + mainClassName = "tv.codely.context.video.module.video.infrastructure.VideoPublisherCliController" +} diff --git a/src/main/java/tv/codely/context/notification/module/push/application/create/SendPushToSubscribersOnVideoPublished.java b/src/main/java/tv/codely/context/notification/module/push/application/create/SendPushToSubscribersOnVideoPublished.java new file mode 100644 index 0000000..ce70b4c --- /dev/null +++ b/src/main/java/tv/codely/context/notification/module/push/application/create/SendPushToSubscribersOnVideoPublished.java @@ -0,0 +1,22 @@ +package tv.codely.context.notification.module.push.application.create; + +import tv.codely.context.video.module.video.domain.VideoPublished; +import tv.codely.shared.application.DomainEventSubscriber; + +public class SendPushToSubscribersOnVideoPublished implements DomainEventSubscriber { + @Override + public Class subscribedTo() { + return VideoPublished.class; + } + + @Override + public void consume(VideoPublished event) { + System.out.println( + String.format( + "Hey! There is a new video with title <%s> and description <%s>", + event.title(), + event.description() + ) + ); + } +} diff --git a/src/main/java/tv/codely/context/notification/module/push/application/find/.gitkeep b/src/main/java/tv/codely/context/notification/module/push/application/find/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/tv/codely/context/notification/module/push/domain/.gitkeep b/src/main/java/tv/codely/context/notification/module/push/domain/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/tv/codely/context/notification/module/push/infrastructure/.gitkeep b/src/main/java/tv/codely/context/notification/module/push/infrastructure/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/tv/codely/context/video/module/video/application/publish/VideoPublisher.java b/src/main/java/tv/codely/context/video/module/video/application/publish/VideoPublisher.java new file mode 100644 index 0000000..8bf8eac --- /dev/null +++ b/src/main/java/tv/codely/context/video/module/video/application/publish/VideoPublisher.java @@ -0,0 +1,23 @@ +package tv.codely.context.video.module.video.application.publish; + +import tv.codely.context.video.module.video.domain.Video; +import tv.codely.context.video.module.video.domain.VideoDescription; +import tv.codely.context.video.module.video.domain.VideoTitle; +import tv.codely.shared.domain.EventBus; + +public final class VideoPublisher { + private final EventBus eventBus; + + public VideoPublisher(EventBus eventBus) { + this.eventBus = eventBus; + } + + public void publish(String rawTitle, String rawDescription) { + final var title = new VideoTitle(rawTitle); + final var description = new VideoDescription(rawDescription); + + final var video = Video.publish(title, description); + + eventBus.publish(video.pullDomainEvents()); + } +} diff --git a/src/main/java/tv/codely/context/video/module/video/domain/Video.java b/src/main/java/tv/codely/context/video/module/video/domain/Video.java new file mode 100644 index 0000000..cb7c4e9 --- /dev/null +++ b/src/main/java/tv/codely/context/video/module/video/domain/Video.java @@ -0,0 +1,23 @@ +package tv.codely.context.video.module.video.domain; + +import tv.codely.shared.domain.AggregateRoot; + +public final class Video extends AggregateRoot { + private final VideoTitle title; + private final VideoDescription description; + + private Video(VideoTitle title, VideoDescription description) { + this.title = title; + this.description = description; + } + + public static Video publish(VideoTitle title, VideoDescription description) { + var video = new Video(title, description); + + var videoCreated = new VideoPublished(title.value(), description.value()); + + video.record(videoCreated); + + return video; + } +} diff --git a/src/main/java/tv/codely/context/video/module/video/domain/VideoDescription.java b/src/main/java/tv/codely/context/video/module/video/domain/VideoDescription.java new file mode 100644 index 0000000..696bda8 --- /dev/null +++ b/src/main/java/tv/codely/context/video/module/video/domain/VideoDescription.java @@ -0,0 +1,28 @@ +package tv.codely.context.video.module.video.domain; + +public final class VideoDescription { + private final String value; + + public VideoDescription(final String value) { + this.value = value; + } + + public String value() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + VideoDescription that = (VideoDescription) o; + + return value.equals(that.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/src/main/java/tv/codely/context/video/module/video/domain/VideoPublished.java b/src/main/java/tv/codely/context/video/module/video/domain/VideoPublished.java new file mode 100644 index 0000000..f527419 --- /dev/null +++ b/src/main/java/tv/codely/context/video/module/video/domain/VideoPublished.java @@ -0,0 +1,45 @@ +package tv.codely.context.video.module.video.domain; + +import tv.codely.shared.domain.DomainEvent; + +public final class VideoPublished implements DomainEvent { + private static final String FULL_QUALIFIED_EVENT_NAME = "codelytv.video.video.event.1.video.published"; + + private final String title; + private final String description; + + public VideoPublished(String title, String description) { + this.title = title; + this.description = description; + } + + public String fullQualifiedEventName() { + return FULL_QUALIFIED_EVENT_NAME; + } + + public String title() { + return title; + } + + public String description() { + return description; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + VideoPublished that = (VideoPublished) o; + + if (!title.equals(that.title)) return false; + return description.equals(that.description); + } + + @Override + public int hashCode() { + int result = title.hashCode(); + result = 31 * result + description.hashCode(); + return result; + } +} diff --git a/src/main/java/tv/codely/context/video/module/video/domain/VideoTitle.java b/src/main/java/tv/codely/context/video/module/video/domain/VideoTitle.java new file mode 100644 index 0000000..92b916e --- /dev/null +++ b/src/main/java/tv/codely/context/video/module/video/domain/VideoTitle.java @@ -0,0 +1,28 @@ +package tv.codely.context.video.module.video.domain; + +public final class VideoTitle { + private final String value; + + public VideoTitle(String value) { + this.value = value; + } + + public String value() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + VideoTitle that = (VideoTitle) o; + + return value.equals(that.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/src/main/java/tv/codely/context/video/module/video/infrastructure/.gitkeep b/src/main/java/tv/codely/context/video/module/video/infrastructure/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/tv/codely/context/video/module/video/infrastructure/VideoPublisherCliController.java b/src/main/java/tv/codely/context/video/module/video/infrastructure/VideoPublisherCliController.java new file mode 100644 index 0000000..768fb44 --- /dev/null +++ b/src/main/java/tv/codely/context/video/module/video/infrastructure/VideoPublisherCliController.java @@ -0,0 +1,24 @@ +package tv.codely.context.video.module.video.infrastructure; + +import tv.codely.context.notification.module.push.application.create.SendPushToSubscribersOnVideoPublished; +import tv.codely.context.video.module.video.application.publish.VideoPublisher; +import tv.codely.shared.application.DomainEventSubscriber; +import tv.codely.shared.domain.EventBus; +import tv.codely.shared.infrastructure.bus.ReactorEventBus; + +import java.util.Set; + +public class VideoPublisherCliController { + public static void main(String[] args) { + final Set subscribers = Set.of( + new SendPushToSubscribersOnVideoPublished() + ); + final EventBus eventBus = new ReactorEventBus(subscribers); + final var videoPublisher = new VideoPublisher(eventBus); + + final var videoTitle = "\uD83C\uDF89 New YouTube.com/CodelyTV video title"; + final var videoDescription = "This should be the video description \uD83D\uDE42"; + + videoPublisher.publish(videoTitle, videoDescription); + } +} diff --git a/src/main/java/tv/codely/java_bootstrap/Greeter.java b/src/main/java/tv/codely/java_bootstrap/Greeter.java deleted file mode 100644 index f56dc9f..0000000 --- a/src/main/java/tv/codely/java_bootstrap/Greeter.java +++ /dev/null @@ -1,9 +0,0 @@ -package tv.codely.java_bootstrap; - -public class Greeter { - - public String greet(String name) { - return "Hello " + name; - } - -} diff --git a/src/main/java/tv/codely/shared/application/DomainEventSubscriber.java b/src/main/java/tv/codely/shared/application/DomainEventSubscriber.java new file mode 100644 index 0000000..099cdaa --- /dev/null +++ b/src/main/java/tv/codely/shared/application/DomainEventSubscriber.java @@ -0,0 +1,9 @@ +package tv.codely.shared.application; + +import tv.codely.shared.domain.DomainEvent; + +public interface DomainEventSubscriber { + Class subscribedTo(); + + void consume(EventType event); +} diff --git a/src/main/java/tv/codely/shared/domain/AggregateRoot.java b/src/main/java/tv/codely/shared/domain/AggregateRoot.java new file mode 100644 index 0000000..82ecbed --- /dev/null +++ b/src/main/java/tv/codely/shared/domain/AggregateRoot.java @@ -0,0 +1,19 @@ +package tv.codely.shared.domain; + +import java.util.LinkedList; +import java.util.List; + +public abstract class AggregateRoot { + private List recordedDomainEvents = new LinkedList<>(); + + final public List pullDomainEvents() { + final var recordedDomainEvents = this.recordedDomainEvents; + this.recordedDomainEvents = new LinkedList<>(); + + return recordedDomainEvents; + } + + final protected void record(DomainEvent event) { + recordedDomainEvents.add(event); + } +} diff --git a/src/main/java/tv/codely/shared/domain/DomainEvent.java b/src/main/java/tv/codely/shared/domain/DomainEvent.java new file mode 100644 index 0000000..563dd6e --- /dev/null +++ b/src/main/java/tv/codely/shared/domain/DomainEvent.java @@ -0,0 +1,5 @@ +package tv.codely.shared.domain; + +public interface DomainEvent { + String fullQualifiedEventName(); +} diff --git a/src/main/java/tv/codely/shared/domain/EventBus.java b/src/main/java/tv/codely/shared/domain/EventBus.java new file mode 100644 index 0000000..31211bc --- /dev/null +++ b/src/main/java/tv/codely/shared/domain/EventBus.java @@ -0,0 +1,7 @@ +package tv.codely.shared.domain; + +import java.util.List; + +public interface EventBus { + void publish(final List events); +} diff --git a/src/main/java/tv/codely/shared/infrastructure/bus/ReactorEventBus.java b/src/main/java/tv/codely/shared/infrastructure/bus/ReactorEventBus.java new file mode 100644 index 0000000..800bccf --- /dev/null +++ b/src/main/java/tv/codely/shared/infrastructure/bus/ReactorEventBus.java @@ -0,0 +1,49 @@ +package tv.codely.shared.infrastructure.bus; + +import reactor.bus.Event; +import reactor.bus.EventBus; +import reactor.bus.selector.Selector; +import reactor.fn.Consumer; +import tv.codely.shared.application.DomainEventSubscriber; +import tv.codely.shared.domain.DomainEvent; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import static reactor.bus.selector.Selectors.$; + +public class ReactorEventBus implements tv.codely.shared.domain.EventBus { + private final EventBus bus; + + public ReactorEventBus(final Set subscribers) { + bus = EventBus.create(); + + subscribers.forEach(this::registerOnEventBus); + } + + @Override + public void publish(final List events) { + events.forEach(this::publish); + } + + private void publish(final DomainEvent event) { + Class eventIdentifier = event.getClass(); + Event wrappedEvent = Event.wrap(event); + + bus.notify(eventIdentifier, wrappedEvent); + } + + private void registerOnEventBus(final DomainEventSubscriber subscriber) { + final Selector eventIdentifier = $(subscriber.subscribedTo()); + + bus.on(eventIdentifier, eventConsumer(subscriber)); + } + + private Consumer eventConsumer(final DomainEventSubscriber subscriber) { + return (Event reactorEvent) -> { + DomainEvent unwrappedEvent = (DomainEvent) reactorEvent.getData(); + subscriber.consume(unwrappedEvent); + }; + } +} diff --git a/src/test/java/tv/codely/context/video/module/video/application/publish/VideoPublisherShould.java b/src/test/java/tv/codely/context/video/module/video/application/publish/VideoPublisherShould.java new file mode 100644 index 0000000..ef09711 --- /dev/null +++ b/src/test/java/tv/codely/context/video/module/video/application/publish/VideoPublisherShould.java @@ -0,0 +1,28 @@ +package tv.codely.context.video.module.video.application.publish; + +import org.junit.jupiter.api.Test; +import tv.codely.context.video.module.video.domain.VideoPublished; +import tv.codely.shared.domain.EventBus; + +import java.util.List; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +final class VideoPublisherShould { + @Test + void publish_the_video_published_domain_event() { + final EventBus eventBus = mock(EventBus.class); + final var videoPublisher = new VideoPublisher(eventBus); + + final var videoTitle = "\uD83C\uDF89 New YouTube.com/CodelyTV video title"; + final var videoDescription = "This should be the video description \uD83D\uDE42"; + + videoPublisher.publish(videoTitle, videoDescription); + + final var expectedVideoCreated = new VideoPublished(videoTitle, videoDescription); + + verify(eventBus).publish(List.of(expectedVideoCreated)); + } + +} diff --git a/src/test/java/tv/codely/java_bootstrap/GreeterShould.java b/src/test/java/tv/codely/java_bootstrap/GreeterShould.java deleted file mode 100644 index 8399e15..0000000 --- a/src/test/java/tv/codely/java_bootstrap/GreeterShould.java +++ /dev/null @@ -1,15 +0,0 @@ -package tv.codely.java_bootstrap; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Test; - -public class GreeterShould { - - @Test - void greet_with_a_hello_message_to_the_name_it_receives() { - Greeter greeter = new Greeter(); - assertEquals("Hello Jhon", greeter.greet("Jhon")); - } - -}