From 6659ace12da67d935513e9c232b466ca9d0e4b81 Mon Sep 17 00:00:00 2001 From: sttk Date: Wed, 21 Dec 2022 09:04:47 +0900 Subject: [PATCH 1/5] comment: added javadoc `@param` comments of ErrHandler --- src/main/java/sabi/ErrHandler.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/sabi/ErrHandler.java b/src/main/java/sabi/ErrHandler.java index 09d063c..2b23a8c 100644 --- a/src/main/java/sabi/ErrHandler.java +++ b/src/main/java/sabi/ErrHandler.java @@ -14,6 +14,9 @@ public interface ErrHandler { /** * Handles an {@link Err} object which will be created rigth after. + * + * @param err An {@link Err} object. + * @param odt {@link OffsetDateTime} object which is the current time. */ void handle(Err err, OffsetDateTime odt); } From 2a72ae9d80c8b275f1f44b6fc4e4c456eae2fa98 Mon Sep 17 00:00:00 2001 From: sttk Date: Wed, 21 Dec 2022 09:06:13 +0900 Subject: [PATCH 2/5] chore: motified package directory name: sabi/nofity -> sabi/notify --- src/main/java/sabi/{nofity => notify}/ErrNotifier.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/java/sabi/{nofity => notify}/ErrNotifier.java (100%) diff --git a/src/main/java/sabi/nofity/ErrNotifier.java b/src/main/java/sabi/notify/ErrNotifier.java similarity index 100% rename from src/main/java/sabi/nofity/ErrNotifier.java rename to src/main/java/sabi/notify/ErrNotifier.java From 2eff7a1dd543df627aaa2996019ec43535eac1db Mon Sep 17 00:00:00 2001 From: sttk Date: Sun, 25 Dec 2022 00:30:34 +0900 Subject: [PATCH 3/5] chore: modified build.sh --- build.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/build.sh b/build.sh index 287c543..42afa34 100755 --- a/build.sh +++ b/build.sh @@ -52,8 +52,6 @@ sver) ;; '') clean - compile - test jar javadoc ;; From 8ee0dfa6b73af8d4907ce830e1c6d4529e1d3ec7 Mon Sep 17 00:00:00 2001 From: sttk Date: Sun, 25 Dec 2022 00:45:28 +0900 Subject: [PATCH 4/5] feat: added main classes of this framework --- src/main/java/sabi/Dax.java | 22 + src/main/java/sabi/DaxBase.java | 204 +++++++++ src/main/java/sabi/DaxConn.java | 31 ++ src/main/java/sabi/DaxSrc.java | 23 + src/main/java/sabi/Logic.java | 28 ++ src/main/java/sabi/Proc.java | 111 +++++ src/main/java/sabi/Runner.java | 124 ++++++ src/test/java/sabi/DaxBaseTest.java | 631 ++++++++++++++++++++++++++++ src/test/java/sabi/ProcTest.java | 296 +++++++++++++ src/test/java/sabi/RunnerTest.java | 128 ++++++ 10 files changed, 1598 insertions(+) create mode 100644 src/main/java/sabi/Dax.java create mode 100644 src/main/java/sabi/DaxBase.java create mode 100644 src/main/java/sabi/DaxConn.java create mode 100644 src/main/java/sabi/DaxSrc.java create mode 100644 src/main/java/sabi/Logic.java create mode 100644 src/main/java/sabi/Proc.java create mode 100644 src/main/java/sabi/Runner.java create mode 100644 src/test/java/sabi/DaxBaseTest.java create mode 100644 src/test/java/sabi/ProcTest.java create mode 100644 src/test/java/sabi/RunnerTest.java diff --git a/src/main/java/sabi/Dax.java b/src/main/java/sabi/Dax.java new file mode 100644 index 0000000..0a58835 --- /dev/null +++ b/src/main/java/sabi/Dax.java @@ -0,0 +1,22 @@ +/* + * Dax class. + * Copyright (C) 2022 Takayuki Sato. All Rights Reserved. + */ +package sabi; + +/** + * Dax is an interface for a set of data accesses, and requires a method: + * {@link #getDaxConn} which gets a connection to an external data access. + */ +public interface Dax { + + /** + * Gets a {@link DaxConn} which is a connection to a data source by specified + * name. + * + * @param name The name of {@link DaxConn} or {@link DaxSrc}. + * @return A {@link DaxConn} object. + * @throws Err If failing to get {@link DaxConn}. + */ + DaxConn getDaxConn(final String name) throws Err; +} diff --git a/src/main/java/sabi/DaxBase.java b/src/main/java/sabi/DaxBase.java new file mode 100644 index 0000000..0e1b6ff --- /dev/null +++ b/src/main/java/sabi/DaxBase.java @@ -0,0 +1,204 @@ +/* + * DaxBase class. + * Copyright (C) 2022 Takayuki Sato. All Rights Reserved. + */ +package sabi; + +import java.util.Map; +import java.util.HashMap; +import java.util.LinkedHashMap; + +/** + * DaxBase manages multiple {@link DaxSrc} and those {@link DaxConn}, and also + * work as an implementation of 'Dax' interface. + */ +public abstract class DaxBase { + + /** + * An error reason which indicates that a specified data source instance is + * not found. + * + * @param name A registered name of a {@link DaxSrc} is not found. + */ + public record DaxSrcIsNotFound(String name) {}; + + /** + * An error reason which indicates that it failed to create a new connection + * to a data source. + * + * @param name A registered name of a {@link DaxSrc} which failed to create + * a {@link DaxConn}. + */ + public record FailToCreateDaxConn(String name) {}; + + /** + * An error reason which indicates that some connections failed th commit. + * + * @param errors A map of which keys are registered names of {@link DaxConn} + * which failed to commit, and of which values are {@link Throwable} thrown + * by {@link DaxConn#commit} methods.. + */ + public record FailToCommitDaxConn(Map errors) {} + + /** + * An error reason which indicates that a {@link DaxConn} of the specified + * name caused an exception. + * + * @param name A name of {@link DaxConn} which caused an exception. + */ + public record CommitExceptionOccurs(String name) {} + + /** The global flag which fixes global {@link DaxSrc} compositions. */ + private static boolean isGlobalDaxSrcsFixed = false; + + /** The global map which composes global {@link DaxSrc} objects. */ + private static final Map globalDaxSrcMap = new LinkedHashMap<>(); + + /** The local flag which fixes local {@link DaxSrc} compositions. */ + private boolean isLocalDaxSrcsFixed = false; + + /** The local map which composes local {@link DaxSrc} objects. */ + private final Map localDaxSrcMap = new LinkedHashMap<>(); + + /** The local map which composes {@link DaxConn} objects. */ + private final Map daxConnMap = new LinkedHashMap<>(); + + /** + * Registers a global {@link DaxSrc} with its name to make enable to use + * {@link DaxSrc} in all transactions. + * + * @param name The name for the argument {@link DaxSrc} and also for a + * {@link DaxConn} created by the argument {@link DaxSrc}. + * This name is used to get a {@link DaxConn} with {@link #getDaxConn} + * method. + * @param ds A {@link DaxSrc} object to be registered globally to enable to + * be used in all transactions. + */ + public static synchronized void addGlobalDaxSrc(final String name, final DaxSrc ds) { + if (!isGlobalDaxSrcsFixed) { + globalDaxSrcMap.put(name, ds); + } + } + + /** + * Makes unable to register any further global {@link DaxSrc}. + */ + public static synchronized void fixGlobalDaxSrcs() { + isGlobalDaxSrcsFixed = true; + } + + /** + * The default constructor of this class. + */ + public DaxBase() {} + + /** + * Registers a local {@link DaxSrc} with a specified name. + * + * @param name The name for the argument {@link DaxSrc} and also for a + * {@link DaxConn} created by the argument {@link DaxSrc}. + * This name is used to get a {@link DaxConn} with {@link #getDaxConn} + * method. + * @param ds A {@link DaxSrc} object to be registered locally to enable to + * be used in only specific transactions. + */ + public void addLocalDaxSrc(final String name, final DaxSrc ds) { + if (!isLocalDaxSrcsFixed) { + localDaxSrcMap.put(name, ds); + } + } + + /** + * Gets a {@link DaxConn} which is a connection to a data source by specified + * name. + * If a {@link DaxConn} is found, this method returns it, but not found, + * this method creates a new one with a local or global {@link DaxSrc} with + * same name. + * If there are both local and global {@link DaxSrc} with same name, the + * local {@link DaxSrc} is used. + * + * @param name The name of {@link DaxConn} or {@link DaxSrc}. + * @return A {@link DaxConn} object. + * @throws Err If the following error occured: + *
    + *
  • {@link sabi.DaxBase.DaxSrcIsNotFound} - + * If {@link DaxSrc} with the specified name is not found.
  • + *
+ */ + public DaxConn getDaxConn(final String name) throws Err { + var conn = this.daxConnMap.get(name); + if (conn != null) { + return conn; + } + + var ds = this.localDaxSrcMap.get(name); + if (ds == null) { + ds = globalDaxSrcMap.get(name); + } + if (ds == null) { + throw new Err(new DaxSrcIsNotFound(name)); + } + + synchronized (this.daxConnMap) { + conn = this.daxConnMap.get(name); + if (conn != null) { + return conn; + } + + try { + conn = ds.createDaxConn(); + } catch (Err e) { + throw new Err(new FailToCreateDaxConn(name), e); + } + + this.daxConnMap.put(name, conn); + } + + return conn; + } + + void begin() { + this.isLocalDaxSrcsFixed = true; + isGlobalDaxSrcsFixed = true; + } + + void commit() throws Err { + var errors = new HashMap(); + + for (var entry : this.daxConnMap.entrySet()) { + try { + var conn = entry.getValue(); + conn.commit(); + } catch (Err err) { + errors.put(entry.getKey(), err); + break; + } catch (Exception exc) { + var err = new Err(new CommitExceptionOccurs(entry.getKey()), exc); + errors.put(entry.getKey(), err); + break; + } + } + + if (!errors.isEmpty()) { + throw new Err(new FailToCommitDaxConn(errors)); + } + } + + void rollback() { + for (var conn : this.daxConnMap.values()) { + try { + conn.rollback(); + } catch (Throwable t) {} + } + } + + void close() { + for (var conn : this.daxConnMap.values()) { + try { + conn.close(); + } catch (Throwable t) {} + } + + this.isLocalDaxSrcsFixed = false; + } +} diff --git a/src/main/java/sabi/DaxConn.java b/src/main/java/sabi/DaxConn.java new file mode 100644 index 0000000..ff24a3f --- /dev/null +++ b/src/main/java/sabi/DaxConn.java @@ -0,0 +1,31 @@ +/* + * DaxConn class. + * Copyright (C) 2022 Takayuki Sato. All Rights Reserved. + */ +package sabi; + +/** + * DaxConn is an interface which represents a connection to a data source. + + * The class inheriting this class requires methods: {@link #commit}, + * {@link #rollback} and {@link #close} to work in a transaction process. + */ +public interface DaxConn { + + /** + * Makes all changes since the previous commit/rollback permanent. + * + * @throws Err If this connection failed to commit changes. + */ + void commit() throws Err; + + /** + * Undoes all changes since the previous commit rollback. + */ + void rollback(); + + /** + * Closes and releases this connection. + */ + void close(); +} diff --git a/src/main/java/sabi/DaxSrc.java b/src/main/java/sabi/DaxSrc.java new file mode 100644 index 0000000..f7baab3 --- /dev/null +++ b/src/main/java/sabi/DaxSrc.java @@ -0,0 +1,23 @@ +/* + * DaxSrc claass. + * Copyright (C) 2022 Takayuki Sato. All Rights Reserved. + */ +package sabi; + +/** + * DaxSrc is an interface which represents a data source like database, etc., + * and creates a {@link DaxConn} to the data source. + * The class inheriting this requires a method: {@link #createDaxConn} to do + * so. + */ +public interface DaxSrc { + + /** + * Creates a {@link DaxConn} object which is a connection to a data source + * which the instance of this class indicates. + * + * @return a {@link DaxConn} object. + * @throws Err If this instance failed to create a {@link DaxConn} object. + */ + DaxConn createDaxConn() throws Err; +} diff --git a/src/main/java/sabi/Logic.java b/src/main/java/sabi/Logic.java new file mode 100644 index 0000000..b7993ec --- /dev/null +++ b/src/main/java/sabi/Logic.java @@ -0,0 +1,28 @@ +/* + * Logic class. + * Copyright (C) 2022 Takayuki Sato. All Rights Reserved. + */ +package sabi; + +/** + * Logic is a functional interface which executes a logical process. + * + * In this class, only logical codes should be written and data access codes + * for external data sources should not. + * Data access codes should be written in methods associated with a dax + * interface which is an argument of {@link #execute} method. + */ +@FunctionalInterface +public interface Logic { + + /** + * Executes the logical process represented by this class. + * + * This method is the entry point of the whole logical process represented by + * this class. + * + * @param dax A data access interface. + * @throws Err If an error occured in an implementation for an argument dax. + */ + void execute(final D dax) throws Err; +} diff --git a/src/main/java/sabi/Proc.java b/src/main/java/sabi/Proc.java new file mode 100644 index 0000000..266ba18 --- /dev/null +++ b/src/main/java/sabi/Proc.java @@ -0,0 +1,111 @@ +/* + * Proc class. + * Copyright (C) 2022 Takayuki Sato. All Rights Reserved. + */ +package sabi; + +/** + * Proc is a class which represents a procedure. + * + * @param A type of dax used for external data accesses. + */ +public class Proc { + + /** + * A error reason which indicates a specified dax does not inherit + * {@link DaxBase} class. + * + * @param daxClass A dax class. + */ + public record DaxDoesNotInheritDaxBase(Class daxClass) {} + + /** A dax object which has all method interfaces used in this procedure. */ + private final D dax; + + /** + * A constructor which takes a dax as an argument. + * + * The class of the specified dax is needed to inherit {@link DaxBase} class. + * + * @param dax A dax object. + * @throws ClassCastException - If the dax is not null and does not inherit + * {@link DaxBase} class. + */ + public Proc(final D dax) { + DaxBase.class.cast(dax); + this.dax = dax; + } + + /** + * Registers a local {@link DaxSrc} with a specified name. + * + * @param name The name for the argument {@link DaxSrc} and also for a + * {@link DaxConn} created by the argument {@link DaxSrc}. + * This name is used to get a {@link DaxConn} with {@link + * DaxBase#getDaxConn} method. + * @param ds A {@link DaxSrc} object to be registered locally to enable to + * be used in only specific transactions. + */ + public void addLocalDaxSrc(final String name, final DaxSrc ds) { + DaxBase.class.cast(dax).addLocalDaxSrc(name, ds); + } + + /** + * Runs logic specified as arguments in a transaction. + * + * @param logics Logic functional interfaces. + * @throws Err If an exception occurs in a logic. + */ + public void runTxn(final Logic ...logics) throws Err { + var base = DaxBase.class.cast(dax); + + try { + base.begin(); + + for (var logic : logics) { + logic.execute(dax); + } + + base.commit(); + + } catch (Err e) { + base.rollback(); + throw e; + + } finally { + base.close(); + } + } + + /** + * Creates a transaction having specified logics. + * + * @param logics {@link Logic} objects. + * @return A {@link Runner} object which processes a transaction. + */ + public Runner txn(final Logic ...logics) { + return new Runner() { + @Override + public void run() throws Err { + var base = DaxBase.class.cast(dax); + + try { + base.begin(); + + for (var logic : logics) { + logic.execute(dax); + } + + base.commit(); + + } catch (Err e) { + base.rollback(); + throw e; + + } finally { + base.close(); + } + } + }; + } +} diff --git a/src/main/java/sabi/Runner.java b/src/main/java/sabi/Runner.java new file mode 100644 index 0000000..8aa1694 --- /dev/null +++ b/src/main/java/sabi/Runner.java @@ -0,0 +1,124 @@ +/* + * Runner class. + * Copyright (C) 2022 Takayuki Sato. All Rights Reserved. + */ +package sabi; + +import java.util.Map; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ExecutionException; + +/** + * Runner is an interface which has {@link #run} method and is runned by + * #runSeq or #runPara method. + */ +public interface Runner { + + /** + * An error reason which indicates some runner which is runned in parallel + * failed. + * + * @param errors A map contains {@link Err} objects with parallelized + * runner's indexes. + */ + public record FailToRunInParallel(Map errors) {}; + + /** + * An error reason which indicates that an exception occurs in parallelized + * runner. + */ + public record RunInParallelExceptionOccurs() {}; + + /** + * Executes a process represented by this class. + * + * @throws Err If an exception occurs in this process. + */ + void run() throws Err; + + /** + * Runs specified runners sequencially. + * + * @param runners {@link Runner}'s variadic arguments. + */ + public static void runSeq(final Runner... runners) throws Err { + for (var runner : runners) { + runner.run(); + } + } + + /** + * Runs specified runners in parallel. + * + * @param runners {@link Runner}'s variadic arguments. + */ + public static void runPara(final Runner... runners) throws Err { + final var executors = Executors.newFixedThreadPool(runners.length); + final var futures = new ArrayList(runners.length); + final var errors = new HashMap(); + try { + for (var runner : runners) { + futures.add(executors.submit(() -> { + runner.run(); + return null; + })); + } + + final var it = futures.iterator(); + for (int i = 0; it.hasNext(); i++) { + try { + it.next().get(); + } catch (ExecutionException exc) { + var cause = exc.getCause(); + if (cause instanceof Err) { + errors.put(i, Err.class.cast(cause)); + } else { + errors.put(i, new Err(new RunInParallelExceptionOccurs(), cause)); + } + } catch (Exception e) { + errors.put(i, new Err(new RunInParallelExceptionOccurs(), e)); + } + } + } finally { + executors.shutdown(); + } + + if (!errors.isEmpty()) { + throw new Err(new FailToRunInParallel(errors)); + } + } + + /** + * Creates a runner which runs multiple runners specified as arguments + * sequencially. + * + * @param runners {@link Runner} objects. + */ + static Runner seq(Runner ...runners) { + return new Runner() { + @Override + public void run() throws Err { + Runner.runSeq(runners); + } + }; + } + + /** + * Creates a runner which runs multiple runners specified as arguments + * in parallel. + * + * @param runners {@link Runner} objects. + */ + static Runner para(Runner ...runners) { + return new Runner() { + @Override + public void run() throws Err { + Runner.runPara(runners); + } + }; + } +} diff --git a/src/test/java/sabi/DaxBaseTest.java b/src/test/java/sabi/DaxBaseTest.java new file mode 100644 index 0000000..c7ca329 --- /dev/null +++ b/src/test/java/sabi/DaxBaseTest.java @@ -0,0 +1,631 @@ +package sabi; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.BeforeEach; + +import java.lang.reflect.Field; + +import java.util.List; +import java.util.ArrayList; +import java.util.Map; + +public class DaxBaseTest { + + final List logs = new ArrayList<>(); + boolean willFailToCreateFooDaxConn = false; + boolean willFailToCommitFooDaxConn = false; + boolean willThrowCommitExceptionOccurs = false; + + final record InvalidDaxConn() {} + + class FooDaxConn implements DaxConn { + String label; + public FooDaxConn() { + this.label = ""; + } + public FooDaxConn(final String label) { + this.label = label; + } + public void commit() throws Err { + if (willFailToCommitFooDaxConn) { + throw new Err(new InvalidDaxConn()); + } + if (willThrowCommitExceptionOccurs) { + throw new RuntimeException(); + } + logs.add("FooDaxConn#commit"); + } + public void rollback() { + logs.add("FooDaxConn#rollback"); + } + public void close() { + logs.add("FooDaxConn#close"); + } + } + + class FooDaxSrc implements DaxSrc { + String label; + public FooDaxSrc() { + this.label = ""; + } + public FooDaxSrc(String label) { + this.label = label; + } + public DaxConn createDaxConn() throws Err { + if (willFailToCreateFooDaxConn) { + throw new Err(new InvalidDaxConn()); + } + return new FooDaxConn(this.label); + } + } + + class BarDaxConn implements DaxConn { + String label; + public BarDaxConn() { + this.label = ""; + } + public BarDaxConn(final String label) { + this.label = label; + } + public void commit() throws Err { + logs.add("BarDaxConn#commit"); + } + public void rollback() { + logs.add("BarDaxConn#rollback"); + } + public void close() { + logs.add("BarDaxConn#close"); + } + } + + class BarDaxSrc implements DaxSrc { + String label; + public BarDaxSrc() { + this.label = ""; + } + public BarDaxSrc(String label) { + this.label = label; + } + public DaxConn createDaxConn() throws Err { + return new BarDaxConn(this.label); + } + } + + + @BeforeEach + void clear() throws Exception { + final var f0 = DaxBase.class.getDeclaredField("isGlobalDaxSrcsFixed"); + f0.setAccessible(true); + f0.setBoolean(null, false); + + final var f1 = DaxBase.class.getDeclaredField("globalDaxSrcMap"); + f1.setAccessible(true); + @SuppressWarnings("unchecked") + var map1 = (Map) f1.get(null); + map1.clear(); + + willFailToCreateFooDaxConn = false; + willFailToCommitFooDaxConn = false; + } + + @Nested + class AddGlobalDaxSrc { + @Test + void should_add_DaxSrc() throws Exception { + final var f0 = DaxBase.class.getDeclaredField("isGlobalDaxSrcsFixed"); + f0.setAccessible(true); + assertThat(f0.getBoolean(null)).isFalse(); + + final var f1 = DaxBase.class.getDeclaredField("globalDaxSrcMap"); + f1.setAccessible(true); + @SuppressWarnings("unchecked") + var map1 = (Map) f1.get(null); + assertThat(map1).isEmpty(); + + DaxBase.addGlobalDaxSrc("foo", new FooDaxSrc()); + + assertThat(f0.getBoolean(null)).isFalse(); + assertThat(map1).hasSize(1); + + DaxBase.addGlobalDaxSrc("bar", new BarDaxSrc()); + + assertThat(f0.getBoolean(null)).isFalse(); + assertThat(map1).hasSize(2); + } + } + + @Nested + class FixGlobalDaxSrcs { + @Test + void should_fix_composition_of_global_DaxSrc() throws Exception { + final var f0 = DaxBase.class.getDeclaredField("isGlobalDaxSrcsFixed"); + f0.setAccessible(true); + assertThat(f0.getBoolean(null)).isFalse(); + + final var f1 = DaxBase.class.getDeclaredField("globalDaxSrcMap"); + f1.setAccessible(true); + @SuppressWarnings("unchecked") + var map1 = (Map) f1.get(null); + assertThat(map1).isEmpty(); + + DaxBase.addGlobalDaxSrc("foo", new FooDaxSrc()); + + assertThat(f0.getBoolean(null)).isFalse(); + assertThat(map1).hasSize(1); + + DaxBase.fixGlobalDaxSrcs(); + + assertThat(f0.getBoolean(null)).isTrue(); + assertThat(map1).hasSize(1); + + DaxBase.addGlobalDaxSrc("bar", new BarDaxSrc()); + + assertThat(f0.getBoolean(null)).isTrue(); + assertThat(map1).hasSize(1); + + f0.setBoolean(null, false); + + assertThat(f0.getBoolean(null)).isFalse(); + assertThat(map1).hasSize(1); + + DaxBase.addGlobalDaxSrc("bar", new BarDaxSrc()); + + assertThat(f0.getBoolean(null)).isFalse(); + assertThat(map1).hasSize(2); + } + } + + @Nested + class AddLocalDaxSrc { + @Test + void should_add_DaxSrc() throws Exception { + var base = new DaxBase() {}; + + final var f0 = DaxBase.class.getDeclaredField("isLocalDaxSrcsFixed"); + f0.setAccessible(true); + assertThat(f0.getBoolean(base)).isFalse(); + + final var f1 = DaxBase.class.getDeclaredField("localDaxSrcMap"); + f1.setAccessible(true); + @SuppressWarnings("unchecked") + var map1 = (Map) f1.get(base); + assertThat(map1).isEmpty(); + + final var f2 = DaxBase.class.getDeclaredField("daxConnMap"); + f2.setAccessible(true); + @SuppressWarnings("unchecked") + var map2 = (Map) f2.get(base); + assertThat(map2).isEmpty(); + + base.addLocalDaxSrc("foo", new FooDaxSrc()); + + assertThat(f0.getBoolean(base)).isFalse(); + assertThat(map1).hasSize(1); + assertThat(map2).isEmpty(); + + base.addLocalDaxSrc("bar", new BarDaxSrc()); + + assertThat(f0.getBoolean(base)).isFalse(); + assertThat(map1).hasSize(2); + assertThat(map2).isEmpty(); + } + } + + @Nested + class Begin { + @Test + void should_fix_composition_of_global_and_local_DaxSrc() throws Exception { + var base = new DaxBase() {}; + + final var f0 = DaxBase.class.getDeclaredField("isGlobalDaxSrcsFixed"); + f0.setAccessible(true); + assertThat(f0.getBoolean(null)).isFalse(); + + final var f1 = DaxBase.class.getDeclaredField("globalDaxSrcMap"); + f1.setAccessible(true); + @SuppressWarnings("unchecked") + var map1 = (Map) f1.get(null); + assertThat(map1).isEmpty(); + + final var f2 = DaxBase.class.getDeclaredField("isLocalDaxSrcsFixed"); + f2.setAccessible(true); + assertThat(f2.getBoolean(base)).isFalse(); + + final var f3 = DaxBase.class.getDeclaredField("localDaxSrcMap"); + f3.setAccessible(true); + @SuppressWarnings("unchecked") + var map3 = (Map) f3.get(base); + assertThat(map3).isEmpty(); + + final var f4 = DaxBase.class.getDeclaredField("daxConnMap"); + f4.setAccessible(true); + @SuppressWarnings("unchecked") + var map4 = (Map) f4.get(base); + assertThat(map4).isEmpty(); + + DaxBase.addGlobalDaxSrc("foo", new FooDaxSrc()); + base.addLocalDaxSrc("foo", new FooDaxSrc()); + + assertThat(f0.getBoolean(null)).isFalse(); + assertThat(map1).hasSize(1); + assertThat(f2.getBoolean(base)).isFalse(); + assertThat(map3).hasSize(1); + assertThat(map4).isEmpty(); + + base.begin(); + + assertThat(f0.getBoolean(null)).isTrue(); + assertThat(map1).hasSize(1); + assertThat(f2.getBoolean(base)).isTrue(); + assertThat(map3).hasSize(1); + assertThat(map4).isEmpty(); + + DaxBase.addGlobalDaxSrc("bar", new BarDaxSrc()); + base.addLocalDaxSrc("bar", new BarDaxSrc()); + + assertThat(f0.getBoolean(null)).isTrue(); + assertThat(map1).hasSize(1); + assertThat(f2.getBoolean(base)).isTrue(); + assertThat(map3).hasSize(1); + assertThat(map4).isEmpty(); + + f2.setBoolean(base, false); + + assertThat(f0.getBoolean(null)).isTrue(); + assertThat(map1).hasSize(1); + assertThat(f2.getBoolean(base)).isFalse(); + assertThat(map3).hasSize(1); + assertThat(map4).isEmpty(); + + DaxBase.addGlobalDaxSrc("bar", new BarDaxSrc()); + base.addLocalDaxSrc("bar", new BarDaxSrc()); + + assertThat(f0.getBoolean(null)).isTrue(); + assertThat(map1).hasSize(1); + assertThat(f2.getBoolean(base)).isFalse(); + assertThat(map3).hasSize(2); + assertThat(map4).isEmpty(); + + f0.setBoolean(null, false); + + assertThat(f0.getBoolean(null)).isFalse(); + assertThat(map1).hasSize(1); + assertThat(f2.getBoolean(base)).isFalse(); + assertThat(map3).hasSize(2); + assertThat(map4).isEmpty(); + + DaxBase.addGlobalDaxSrc("bar", new BarDaxSrc()); + + assertThat(f0.getBoolean(null)).isFalse(); + assertThat(map1).hasSize(2); + assertThat(f2.getBoolean(base)).isFalse(); + assertThat(map3).hasSize(2); + assertThat(map4).isEmpty(); + } + } + + @Nested + class GetDaxConn { + @Test + void should_get_DaxConn_with_local_DaxSrc() throws Exception { + var base = new DaxBase() {}; + + final var f0 = DaxBase.class.getDeclaredField("isGlobalDaxSrcsFixed"); + f0.setAccessible(true); + assertThat(f0.getBoolean(null)).isFalse(); + + final var f1 = DaxBase.class.getDeclaredField("globalDaxSrcMap"); + f1.setAccessible(true); + @SuppressWarnings("unchecked") + var map1 = (Map) f1.get(null); + assertThat(map1).isEmpty(); + + final var f2 = DaxBase.class.getDeclaredField("isLocalDaxSrcsFixed"); + f2.setAccessible(true); + assertThat(f2.getBoolean(base)).isFalse(); + + final var f3 = DaxBase.class.getDeclaredField("localDaxSrcMap"); + f3.setAccessible(true); + @SuppressWarnings("unchecked") + var map3 = (Map) f3.get(base); + assertThat(map3).isEmpty(); + + final var f4 = DaxBase.class.getDeclaredField("daxConnMap"); + f4.setAccessible(true); + @SuppressWarnings("unchecked") + var map4 = (Map) f4.get(base); + assertThat(map4).isEmpty(); + + try { + base.getDaxConn("foo"); + fail(); + } catch (Err e) { + var reason = DaxBase.DaxSrcIsNotFound.class.cast(e.getReason()); + assertThat(reason.name()).isEqualTo("foo"); + } + + base.addLocalDaxSrc("foo", new FooDaxSrc()); + + assertThat(f0.getBoolean(null)).isFalse(); + assertThat(map1).hasSize(0); + assertThat(f2.getBoolean(base)).isFalse(); + assertThat(map3).hasSize(1); + assertThat(map4).isEmpty(); + + final var conn = base.getDaxConn("foo"); + assertThat(conn).isInstanceOf(FooDaxConn.class); + + final var conn1 = base.getDaxConn("foo"); + assertThat(conn1).isEqualTo(conn); + } + + @Test + void should_get_DaxConn_with_global_DaxSrc() throws Exception { + var base = new DaxBase() {}; + + final var f0 = DaxBase.class.getDeclaredField("isGlobalDaxSrcsFixed"); + f0.setAccessible(true); + assertThat(f0.getBoolean(null)).isFalse(); + + final var f1 = DaxBase.class.getDeclaredField("globalDaxSrcMap"); + f1.setAccessible(true); + @SuppressWarnings("unchecked") + var map1 = (Map) f1.get(null); + assertThat(map1).isEmpty(); + + final var f2 = DaxBase.class.getDeclaredField("isLocalDaxSrcsFixed"); + f2.setAccessible(true); + assertThat(f2.getBoolean(base)).isFalse(); + + final var f3 = DaxBase.class.getDeclaredField("localDaxSrcMap"); + f3.setAccessible(true); + @SuppressWarnings("unchecked") + var map3 = (Map) f3.get(base); + assertThat(map3).isEmpty(); + + final var f4 = DaxBase.class.getDeclaredField("daxConnMap"); + f4.setAccessible(true); + @SuppressWarnings("unchecked") + var map4 = (Map) f4.get(base); + assertThat(map4).isEmpty(); + + try { + base.getDaxConn("foo"); + fail(); + } catch (Err e) { + var reason = DaxBase.DaxSrcIsNotFound.class.cast(e.getReason()); + assertThat(reason.name()).isEqualTo("foo"); + } + + DaxBase.addGlobalDaxSrc("foo", new FooDaxSrc()); + + assertThat(f0.getBoolean(null)).isFalse(); + assertThat(map1).hasSize(1); + assertThat(f2.getBoolean(base)).isFalse(); + assertThat(map3).hasSize(0); + assertThat(map4).isEmpty(); + + final var conn = base.getDaxConn("foo"); + assertThat(conn).isInstanceOf(FooDaxConn.class); + + final var conn1 = base.getDaxConn("foo"); + assertThat(conn1).isEqualTo(conn); + } + + @Test + void should_take_local_DaxSrc_priority_of_global_DaxSrc_with_same_name() throws Exception { + var base = new DaxBase() {}; + + final var f0 = DaxBase.class.getDeclaredField("isGlobalDaxSrcsFixed"); + f0.setAccessible(true); + assertThat(f0.getBoolean(null)).isFalse(); + + final var f1 = DaxBase.class.getDeclaredField("globalDaxSrcMap"); + f1.setAccessible(true); + @SuppressWarnings("unchecked") + var map1 = (Map) f1.get(null); + assertThat(map1).isEmpty(); + + final var f2 = DaxBase.class.getDeclaredField("isLocalDaxSrcsFixed"); + f2.setAccessible(true); + assertThat(f2.getBoolean(base)).isFalse(); + + final var f3 = DaxBase.class.getDeclaredField("localDaxSrcMap"); + f3.setAccessible(true); + @SuppressWarnings("unchecked") + var map3 = (Map) f3.get(base); + assertThat(map3).isEmpty(); + + final var f4 = DaxBase.class.getDeclaredField("daxConnMap"); + f4.setAccessible(true); + @SuppressWarnings("unchecked") + var map4 = (Map) f4.get(base); + assertThat(map4).isEmpty(); + + DaxBase.addGlobalDaxSrc("foo", new FooDaxSrc("global")); + base.addLocalDaxSrc("foo", new FooDaxSrc("local")); + + assertThat(f0.getBoolean(null)).isFalse(); + assertThat(map1).hasSize(1); + assertThat(f2.getBoolean(base)).isFalse(); + assertThat(map3).hasSize(1); + assertThat(map4).isEmpty(); + + final var conn = base.getDaxConn("foo"); + + assertThat(conn).isInstanceOf(FooDaxConn.class); + assertThat(FooDaxConn.class.cast(conn).label).isEqualTo("local"); + } + + @Test + void should_throw_Err_if_failing_to_create_DaxConn() throws Exception { + var base = new DaxBase() {}; + + final var f0 = DaxBase.class.getDeclaredField("isGlobalDaxSrcsFixed"); + f0.setAccessible(true); + assertThat(f0.getBoolean(null)).isFalse(); + + final var f1 = DaxBase.class.getDeclaredField("globalDaxSrcMap"); + f1.setAccessible(true); + @SuppressWarnings("unchecked") + var map1 = (Map) f1.get(null); + assertThat(map1).isEmpty(); + + final var f2 = DaxBase.class.getDeclaredField("isLocalDaxSrcsFixed"); + f2.setAccessible(true); + assertThat(f2.getBoolean(base)).isFalse(); + + final var f3 = DaxBase.class.getDeclaredField("localDaxSrcMap"); + f3.setAccessible(true); + @SuppressWarnings("unchecked") + var map3 = (Map) f3.get(base); + assertThat(map3).isEmpty(); + + final var f4 = DaxBase.class.getDeclaredField("daxConnMap"); + f4.setAccessible(true); + @SuppressWarnings("unchecked") + var map4 = (Map) f4.get(base); + assertThat(map4).isEmpty(); + + base.addLocalDaxSrc("foo", new FooDaxSrc("local")); + + assertThat(f0.getBoolean(null)).isFalse(); + assertThat(map1).hasSize(0); + assertThat(f2.getBoolean(base)).isFalse(); + assertThat(map3).hasSize(1); + assertThat(map4).isEmpty(); + + willFailToCreateFooDaxConn = true; + + try { + base.getDaxConn("foo"); + fail(); + } catch (Err e) { + var reason = DaxBase.FailToCreateDaxConn.class.cast(e.getReason()); + assertThat(reason.name()).isEqualTo("foo"); + assertThat(e.get("name")).isEqualTo("foo"); + assertThat(e.getCause().toString()).isEqualTo("sabi.Err: {reason=InvalidDaxConn}"); + } + } + } + + @Nested + class Commit { + @Test + void should_commit() throws Exception { + var base = new DaxBase() {}; + DaxBase.addGlobalDaxSrc("foo", new FooDaxSrc()); + base.addLocalDaxSrc("bar", new BarDaxSrc()); + + base.getDaxConn("foo"); + base.getDaxConn("bar"); + base.commit(); + + assertThat(logs).containsOnly( + "FooDaxConn#commit", + "BarDaxConn#commit" + ); + } + + @Test + void should_throw_Err_if_failing_to_commit() throws Exception { + var base = new DaxBase() {}; + + DaxBase.addGlobalDaxSrc("foo", new FooDaxSrc()); + base.addLocalDaxSrc("bar", new BarDaxSrc()); + + willFailToCommitFooDaxConn = true; + + base.getDaxConn("foo"); + base.getDaxConn("bar"); + + try { + base.commit(); + fail(); + } catch (Err e) { + var reason = DaxBase.FailToCommitDaxConn.class.cast(e.getReason()); + @SuppressWarnings("unchecked") + var errs = (Map) reason.errors(); + assertThat(errs).hasSize(1); + assertThat(errs.get("foo").toString()).isEqualTo("sabi.Err: {reason=InvalidDaxConn}"); + } + + assertThat(logs).isEmpty(); + } + + @Test + void should_throw_Err_if_runtime_exception_occurs() throws Exception { + var base = new DaxBase() {}; + + DaxBase.addGlobalDaxSrc("foo", new FooDaxSrc()); + base.addLocalDaxSrc("bar", new BarDaxSrc()); + + willThrowCommitExceptionOccurs = true; + + base.getDaxConn("foo"); + base.getDaxConn("bar"); + + try { + base.commit(); + fail(); + } catch (Err e) { + var reason = DaxBase.FailToCommitDaxConn.class.cast(e.getReason()); + @SuppressWarnings("unchecked") + var errs = (Map) reason.errors(); + assertThat(errs).hasSize(1); + assertThat(errs.get("foo").toString()).isEqualTo("sabi.Err: {reason=CommitExceptionOccurs, name=foo, cause=java.lang.RuntimeException}"); + } + + assertThat(logs).isEmpty(); + } + } + + @Nested + class Rollback { + @Test + void should_rollback() throws Exception { + var base = new DaxBase() {}; + + DaxBase.addGlobalDaxSrc("foo", new FooDaxSrc()); + base.addLocalDaxSrc("bar", new BarDaxSrc()); + + willFailToCommitFooDaxConn = true; + + base.getDaxConn("foo"); + base.getDaxConn("bar"); + + base.rollback(); + + assertThat(logs).containsOnly( + "FooDaxConn#rollback", + "BarDaxConn#rollback" + ); + } + } + + @Nested + class Close { + @Test + void should_close() throws Exception { + var base = new DaxBase() {}; + + DaxBase.addGlobalDaxSrc("foo", new FooDaxSrc()); + base.addLocalDaxSrc("bar", new BarDaxSrc()); + + willFailToCommitFooDaxConn = true; + + base.getDaxConn("foo"); + base.getDaxConn("bar"); + + base.close(); + + assertThat(logs).containsOnly( + "FooDaxConn#close", + "BarDaxConn#close" + ); + } + } +} diff --git a/src/test/java/sabi/ProcTest.java b/src/test/java/sabi/ProcTest.java new file mode 100644 index 0000000..ac952d5 --- /dev/null +++ b/src/test/java/sabi/ProcTest.java @@ -0,0 +1,296 @@ +package sabi; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.BeforeEach; + +import java.util.List; +import java.util.ArrayList; + +public class ProcTest { + + @Nested + class Constructor { + interface MyDax { + String getData() throws Err; + void setData(String data) throws Err; + } + class MyDaxImpl extends DaxBase implements MyDax { + String greeting = ""; + public String getData() throws Err { + return "world"; + } + public void setData(String v) throws Err { + greeting = "hello, " + v; + } + } + + @Test + void should_create_an_instance() throws Err { + var proc = new Proc(new MyDaxImpl()); + assertThat(proc).isNotNull(); + } + + class MyDaxImplButNotDaxBase implements MyDax { + String greeting = ""; + public String getData() throws Err { + return "world"; + } + public void setData(String v) throws Err { + greeting = "hello, " + v; + } + } + + @Test + void should_throw_a_err_if_an_argument_is_not_DaxBase() throws Err { + var dax = new MyDaxImplButNotDaxBase(); + try { + new Proc(dax); + fail(); + } catch (ClassCastException e) { + assertThat(e.getMessage()).contains( + "sabi.ProcTest$Constructor$MyDaxImplButNotDaxBase", + "sabi.DaxBase" + ); + } + } + } + + @Nested + class RunTxn_without_DaxConn { + + interface MyDax { + String getData() throws Err; + void setData(String data) throws Err; + } + + class MyDaxImpl extends DaxBase implements MyDax { + String greeting = ""; + public String getData() throws Err { + return "world"; + } + public void setData(String v) throws Err { + greeting = "hello, " + v; + } + } + + @Test + void should_run_txn_with_runTxn() throws Exception { + var daxImpl = new MyDaxImpl(); + var proc = new Proc(daxImpl); + proc.runTxn(dax -> { + var data = dax.getData(); + dax.setData(data); + }); + assertThat(daxImpl.greeting).isEqualTo("hello, world"); + } + + @Test + void should_run_txn_with_txn() throws Exception { + var daxImpl = new MyDaxImpl(); + var proc = new Proc(daxImpl); + var runner = proc.txn(dax -> { + var data = dax.getData(); + dax.setData(data); + }); + runner.run(); + assertThat(daxImpl.greeting).isEqualTo("hello, world"); + } + } + + @Nested + class RunTxn_and_txn_with_DaxConn { + record FailToDoSomething() {}; + List logs = new ArrayList<>(); + boolean willFailToDoSomething = false; + + class FooDaxConn implements DaxConn { + @Override + public void commit() throws Err { + logs.add("FooDaxConn#commit"); + } + @Override + public void rollback() { + logs.add("FooDaxConn#rollback"); + } + @Override + public void close() { + logs.add("FooDaxConn#close"); + } + public String fetchData() throws Err { + return "world"; + } + public boolean isError() { + return willFailToDoSomething; + } + } + class FooDaxSrc implements DaxSrc { + @Override + public DaxConn createDaxConn() throws Err { + return new FooDaxConn(); + } + } + interface FooGetDax extends MyDax { + DaxConn getDaxConn(String name) throws Err; + default String getData() throws Err { + var conn = FooDaxConn.class.cast(getDaxConn("foo")); + if (conn.isError()) { + throw new Err(new FailToDoSomething()); + } + return conn.fetchData(); + } + } + + class BarDaxConn implements DaxConn { + BarDaxSrc ds; + @Override + public void commit() throws Err { + logs.add("BarDaxConn#commit"); + } + @Override + public void rollback() { + logs.add("BarDaxConn#rollback"); + } + @Override + public void close() { + logs.add("BarDaxConn#close"); + } + public void saveData(String data) throws Err { + ds.data = data; + } + } + class BarDaxSrc implements DaxSrc { + String data = ""; + @Override + public DaxConn createDaxConn() throws Err { + var conn = new BarDaxConn(); + conn.ds = this; + return conn; + } + } + interface BarSetDax extends MyDax { + DaxConn getDaxConn(String name) throws Err; + default void setData(String data) throws Err { + var conn = BarDaxConn.class.cast(getDaxConn("bar")); + conn.saveData("hello, " + data); + } + } + + interface MyDax { + String getData() throws Err; + void setData(String data) throws Err; + } + class MyDaxImpl extends DaxBase implements MyDax, + FooGetDax, BarSetDax {} + + @Test + void should_run_and_commit_txn_with_runTxn() throws Err { + var fooDs = new FooDaxSrc(); + var barDs = new BarDaxSrc(); + + var daxImpl = new MyDaxImpl(); + var proc = new Proc(daxImpl); + proc.addLocalDaxSrc("foo", fooDs); + proc.addLocalDaxSrc("bar", barDs); + + proc.runTxn(dax -> { + var data = dax.getData(); + dax.setData(data); + }); + assertThat(barDs.data).isEqualTo("hello, world"); + assertThat(logs).containsExactly( + "FooDaxConn#commit", + "BarDaxConn#commit", + "FooDaxConn#close", + "BarDaxConn#close" + ); + } + + @Test + void should_run_and_rollback_txn_with_runTxn() throws Err { + var fooDs = new FooDaxSrc(); + var barDs = new BarDaxSrc(); + + var daxImpl = new MyDaxImpl(); + var proc = new Proc(daxImpl); + proc.addLocalDaxSrc("foo", fooDs); + proc.addLocalDaxSrc("bar", barDs); + + willFailToDoSomething = true; + + try { + proc.runTxn(dax -> { + var data = dax.getData(); + dax.setData(data); + }); + fail(); + } catch (Err err) { + assertThat(err.getReason()).isInstanceOf(FailToDoSomething.class); + } + assertThat(barDs.data).isEqualTo(""); + assertThat(logs).containsExactly( + "FooDaxConn#rollback", + "FooDaxConn#close" + ); + } + + @Test + void should_run_and_commit_txn_with_txn() throws Err { + var fooDs = new FooDaxSrc(); + var barDs = new BarDaxSrc(); + + var daxImpl = new MyDaxImpl(); + var proc = new Proc(daxImpl); + proc.addLocalDaxSrc("foo", fooDs); + proc.addLocalDaxSrc("bar", barDs); + + var runner = proc.txn(dax -> { + var data = dax.getData(); + dax.setData(data); + }); + + runner.run(); + + assertThat(barDs.data).isEqualTo("hello, world"); + assertThat(logs).containsExactly( + "FooDaxConn#commit", + "BarDaxConn#commit", + "FooDaxConn#close", + "BarDaxConn#close" + ); + } + + @Test + void should_run_and_rollback_txn_with_txn() throws Err { + var fooDs = new FooDaxSrc(); + var barDs = new BarDaxSrc(); + + var daxImpl = new MyDaxImpl(); + var proc = new Proc(daxImpl); + proc.addLocalDaxSrc("foo", fooDs); + proc.addLocalDaxSrc("bar", barDs); + + willFailToDoSomething = true; + + var runner = proc.txn(dax -> { + var data = dax.getData(); + dax.setData(data); + }); + + try { + runner.run(); + fail(); + } catch (Err err) { + assertThat(err.getReason()).isInstanceOf(FailToDoSomething.class); + } + assertThat(barDs.data).isEqualTo(""); + assertThat(logs).containsExactly( + "FooDaxConn#rollback", + "FooDaxConn#close" + ); + } + } +} diff --git a/src/test/java/sabi/RunnerTest.java b/src/test/java/sabi/RunnerTest.java new file mode 100644 index 0000000..e2d216b --- /dev/null +++ b/src/test/java/sabi/RunnerTest.java @@ -0,0 +1,128 @@ +package sabi; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.BeforeEach; + +import java.util.ArrayList; + +public class RunnerTest { + + record FailToDoSomething() {} + + @Nested + class RunSeq { + @Test + void should_run_argument_runners_sequencially() throws Exception { + var logs = new ArrayList(); + Runner.runSeq(() -> logs.add("1"), () -> logs.add("2")); + assertThat(logs).containsExactly("1", "2"); + } + + @Test + void should_throw_an_Err_if_one_of_runners_cause_an_Err() { + var logs = new ArrayList(); + try { + Runner.runSeq(() -> { + logs.add("1"); + }, () -> { + throw new Err(new FailToDoSomething()); + }); + } catch (Err e) { + assertThat(e.getReason()).isInstanceOf(FailToDoSomething.class); + } + assertThat(logs).containsExactly("1"); + } + } + + @Nested + class RunPara { + @Test + void should_run_argument_runners_in_parallel() throws Exception { + var logs = new ArrayList(); + Runner.runPara(() -> { + try { Thread.sleep(100); } catch (InterruptedException e) {} + logs.add("1"); + }, () -> { + try { Thread.sleep(20); } catch (InterruptedException e) {} + logs.add("2"); + }); + assertThat(logs).containsExactly("2", "1"); + } + + @Test + void should_throw_an_Err_if_one_of_runners_cause_an_Err() { + var logs = new ArrayList(); + try { + Runner.runPara(() -> { + throw new Err(new FailToDoSomething()); + }, () -> { + logs.add("2"); + }); + } catch (Err e) { + assertThat(e.getReason()) + .isInstanceOf(Runner.FailToRunInParallel.class); + var reason = Runner.FailToRunInParallel.class.cast(e.getReason()); + assertThat(reason.errors()).hasSize(1); + assertThat(reason.errors().get(0).getReason()) + .isInstanceOf(FailToDoSomething.class); + } + assertThat(logs).containsExactly("2"); + } + + @Test + void should_throw_an_Err_if_one_of_runners_cause_an_RuntimeException() { + var logs = new ArrayList(); + try { + Runner.runPara(() -> { + throw new RuntimeException(); + }, () -> { + logs.add("2"); + }); + } catch (Err e) { + assertThat(e.getReason()) + .isInstanceOf(Runner.FailToRunInParallel.class); + var reason = Runner.FailToRunInParallel.class.cast(e.getReason()); + assertThat(reason.errors()).hasSize(1); + assertThat(reason.errors().get(0).getReason()) + .isInstanceOf(Runner.RunInParallelExceptionOccurs.class); + assertThat(reason.errors().get(0).getCause()) + .isInstanceOf(RuntimeException.class); + } + assertThat(logs).containsExactly("2"); + } + } + + @Nested + class Seq { + @Test + void should_run_holding_runners_sequentially() throws Exception { + var logs = new ArrayList(); + var runner = Runner.seq(() -> logs.add("1"), () -> logs.add("2")); + assertThat(logs).isEmpty(); + runner.run(); + assertThat(logs).containsExactly("1", "2"); + } + } + + @Nested + class Para { + @Test + void should_run_holding_runners_in_parallel() throws Exception { + var logs = new ArrayList(); + var runner = Runner.para(() -> { + try { Thread.sleep(100); } catch (InterruptedException e) {} + logs.add("1"); + }, () -> { + try { Thread.sleep(20); } catch (InterruptedException e) {} + logs.add("2"); + }); + assertThat(logs).isEmpty(); + runner.run(); + assertThat(logs).containsExactly("2", "1"); + } + } +} From 94662ba6d426c623a94610cafaa6d3fffe6734d3 Mon Sep 17 00:00:00 2001 From: sttk Date: Mon, 2 Jan 2023 23:49:47 +0900 Subject: [PATCH 5/5] doc: modified README.md --- README.md | 179 ++++++++++++++++++++++++++++++-- src/main/java/sabi/DaxBase.java | 15 ++- 2 files changed, 179 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 2d59f53..d6568ac 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ A small framework for Java applications. - [What is this?](#what-is-this) - [Usage](#usage) +- [Native build](#native-build) - [Supporting JDK versions](#support-jdk-versions) - [License](#license) @@ -17,17 +18,180 @@ And to operate data, procedures includes data accesses, then the rest of procedu Therefore, a program consists of logics, data accesses and data. This package is an application framework which explicitly separates procedures into logics and data accesses as layers. -By using this framework, we can remove codes for data accesses from logic parts, and write only specific codes for each data source (e.g. database, messaging services files, and so on) in data access parts. +By using this framework, we can remove codes for data accesses from logic parts, and write only specific codes for each data source (e.g. database, messaging services, files, and so on) in data access parts. Moreover, by separating as layers, applications using this framework can change data sources easily by switching data access parts. ## Usage -### Write logic - -### Write dax - -### Write procedure +This framework enables to write codes and unit tests of logic parts and data access parts separately. + +### Writing logic + +A logic part is implemented as an instance of `Logic` functional interface. +This `execute` method takes a dax, which is an abbreviation of 'data access', as an argument. +A dax has all methods to be used in a logic, and each method is associated with each data access procedure to target data sources. +Since a dax conceals its data access procedures, only logical procedure appears in a logic part. +In a logic part, these are no concern where a data comes from and a data goes to. + +For example, `GreetLogic` a logic class and `GreetDax` is a dax interface: + +``` +public interface GreetDax { + String getName() throws Err; + void say(String greeting) throws Err; +} +``` +``` +public class GreetLogic implements Logic { + @Override + public void execute(final GreetDax dax) throws Err { + final String name = dax.getName(); + dax.say("Hello, " + name); + } +} +``` + +In `GreetLogic`, there are no detail codes for getting name and putting greeting. +In this logic class, it's only concern to convert a name into a greeting. + +### Writing dax for unit tests of logic. + +To test a logic, the simplest dax implementation is what using a map. +The following code is an example which implements two methods: `getName` and `say` which are same to `GreetDax` interface above. + +``` +public class MapDax extends DaxBase implements GreetDax { + Map map = new HashMap<>(); + + public record NoName() {}; // An error reason when getting no name. + + public String getName() throws Err { + try { + return this.map.get("name"); + } catch (Exception e) { + throw new Err(new NoName(), e); + } + } + + public void say(final String greeting) throws Err { + this.map.put("greeting", greeting); + } +} +``` + +And the following code is an example of a test case. + +``` +public class GreetLogicTest { + @Test + void executeLogic() throws Err { + var dax = new MapDax(); + dax.map.put("name", "World"); + + var proc = new Proc(dax); + proc.runTxn(new GreetLogic()); + + assertThat(dax.map.get("greeting")).isEqualTo("Hello, World"); + } +} +``` + +### Writing dax for real data accesses + +An actual dax ordinarily consists of multiple sub dax by input sources and output destinations. + +The following code is an example of `dax` with no external data source. +This `dax` outputs a greeting to standard output. + +``` +public interface SayConsoleDax extends GreetDax { + @Override + default void say(final String text) throws Err { + System.out.println(text); + } +} +``` + +And the following code is an example of a `dax` with an external data source. +This `UserJdbcDax` accesses to a dataase and provides an implementation of `getName` method of `GreetDax`. + +``` +public interface UserJdbcDax extends JdbcDax, GreetDax { + public record NoUser() {} + public record FailToQueryUserName() {} + + default String getName() throws Err { + try ( + var conn = this.getJdbcDaxConn("jdbc").getConnection(); + var stmt = conn.prepareStatement("SELECT username FROM users LIMIT 1") + ) { + try (var rs = stmt.executeQuery()) { + if (rs.next()) { + return rs.getString("username"); + } + throw new Err(new NoUser()); + } + } catch (Exception e) { + throw new Err(new FailToQueryUserName(), e); + } + } +} +``` + +### Mapping dax interface and implementations + +A `dax` interface can be related to multiple `dax` implementations. + +In the following code, `getName` method of `GreetDax` interface is corresponded to the same named method of `UserJdbcDax`, and `say` method of `GreetDax` interface is corresponded to the same named method of `SayConsoleDax`. + +``` +class GreetDaxImpl extends DaxBase + implements SayConsoleDax, UserJdbcDax {} + +public class GreetProc extends Proc { + public GreetProc() { + super(new GreetDaxImpl()); + } +} +``` + +### Executing logic + +The following code implements a `main` function which execute a `GreetLogic`. +`GreetLogic` is executed in a transaction process by `Proc#RunTxn`, so the database update can be rollbacked if an error is occured. + +The static block registers a `JdbcDaxSrc` which creates a `JdbcDaxConn` which connects to a database. +The `JdbcDaxConn` is registered with a name `"jdbc"` and is obtained by `getJdbcDaxConn("sql")` in `UserJdbcDax#getName`. + +``` +public class GreetApp { + static { + DaxBase.addGlobalDaxSrc("jdbc", new JdbcDaxSrc("database-url")); + DaxBase.fixGlobalDaxSrcs(); + } + + public static void main(String[] args) { + var proc = new GreetProc(); + try { + proc.runTxn(new GreetLogic()); + } catch (Exception e) { + System.exit(1); + } + } +} +``` + + +## Native build + +This framework intends to support native build with GraalVM. +This framework does not use Java reflections, and all `dax` implementations should not use them, too. +However, some of client libraries provided for data sources might use them, and it might be needed to prepare a `reflect-config.json` file. + +See the following pages to build native image with Maven or Gradle. +- [Native image building with Maven plugin](https://www.graalvm.org/dev/reference-manual/native-image/guides/use-native-image-maven-plugin/) +- [Native image building with Gradle plugin](https://graalvm.github.io/native-build-tools/latest/gradle-plugin.html) ## Supporting JDK versions @@ -38,10 +202,11 @@ This framework supports JDK 17 or later. - GraalVM CE 22.1.0 (OpenJDK 17.0.3) + ## License -Copyright (C) 2022 Takayuki Sato +Copyright (C) 2022-2023 Takayuki Sato This program is free software under MIT License.
See the file LICENSE in this distribution for more details. diff --git a/src/main/java/sabi/DaxBase.java b/src/main/java/sabi/DaxBase.java index 0e1b6ff..36ff510 100644 --- a/src/main/java/sabi/DaxBase.java +++ b/src/main/java/sabi/DaxBase.java @@ -10,13 +10,13 @@ /** * DaxBase manages multiple {@link DaxSrc} and those {@link DaxConn}, and also - * work as an implementation of 'Dax' interface. + * works as an implementation of 'Dax' interface. */ public abstract class DaxBase { /** - * An error reason which indicates that a specified data source instance is - * not found. + * An error reason which indicates that a specified {@link DaxSrc} instance + * is not found. * * @param name A registered name of a {@link DaxSrc} is not found. */ @@ -26,17 +26,16 @@ public record DaxSrcIsNotFound(String name) {}; * An error reason which indicates that it failed to create a new connection * to a data source. * - * @param name A registered name of a {@link DaxSrc} which failed to create - * a {@link DaxConn}. + * @param name A registered name of a {@link DaxSrc} which failed to create * a {@link DaxConn}. */ public record FailToCreateDaxConn(String name) {}; /** - * An error reason which indicates that some connections failed th commit. + * An error reason which indicates that some connections failed to commit. * * @param errors A map of which keys are registered names of {@link DaxConn} - * which failed to commit, and of which values are {@link Throwable} thrown - * by {@link DaxConn#commit} methods.. + * which failed to commit, and of which values are {@link Err} thrown by + * {@link DaxConn#commit} methods. */ public record FailToCommitDaxConn(Map errors) {}