Skip to content

Commit ceea5e2

Browse files
authored
Merge pull request #195 from teaql/feat/business-clock-194
Make Java business time context-owned
2 parents d2a0563 + ac21d24 commit ceea5e2

6 files changed

Lines changed: 104 additions & 6 deletions

File tree

‎teaql-core/src/main/java/io/teaql/core/UserContext.java‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,18 @@ default ResolvedRoundTripReference resolveReference(
280280
this, trustedReferencePrincipal(), scope, reference, expectedEntityType);
281281
}
282282

283+
/**
284+
* Returns the domain-visible time for this operation. Operational clocks
285+
* used for timeouts, durations and logging are intentionally separate.
286+
*/
287+
default java.time.LocalDateTime businessTime() {
288+
BusinessClock clock = capability(BusinessClock.class);
289+
if (clock == null) {
290+
throw new TeaQLRuntimeException("BusinessClock capability is not registered");
291+
}
292+
return clock.businessDateTime(this);
293+
}
294+
283295
/**
284296
* Returns the Dynamic Fields facade for reading/writing dynamic field values.
285297
* The facade is resolved via {@link #capability(Class)} and must be registered

‎teaql-core/src/main/java/io/teaql/core/businessid/BusinessClock.java‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,17 @@
22

33
import io.teaql.core.UserContext;
44
import java.time.LocalDate;
5+
import java.time.LocalDateTime;
56

7+
/** Supplies domain-visible time through the current operation context. */
68
public interface BusinessClock {
79
LocalDate businessDate(UserContext context);
10+
11+
/**
12+
* Returns the complete business date-time. Existing date-only clock
13+
* implementations remain deterministic and resolve to start of day.
14+
*/
15+
default LocalDateTime businessDateTime(UserContext context) {
16+
return businessDate(context).atStartOfDay();
17+
}
818
}

‎teaql-runtime/src/main/java/io/teaql/runtime/DefaultUserContext.java‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ public <T> T capability(Class<T> capabilityType) {
158158
value = runtime.getIdGenerationService();
159159
}
160160
if (value == null && capabilityType == BusinessClock.class) {
161-
value = SystemBusinessClock.INSTANCE;
161+
value = runtime.getBusinessClock();
162162
}
163163
if (value == null && capabilityType == BusinessIdProfileFactory.class) {
164164
value = new DefaultBusinessIdProfileFactory();
@@ -341,10 +341,10 @@ private static String sqlOperation(io.teaql.core.ExecutionMetadata metadata) {
341341
@SuppressWarnings("unchecked")
342342
@Override
343343
public final <T> T evaluate(String expression, Object... args) {
344-
// Built-in: "now" returns the current local date-time.
344+
// Built-in: "now" comes from the context-owned business clock.
345345
if ("now".equalsIgnoreCase(expression)) {
346346
Object captured = getAttribute(io.teaql.core.checker.Checker.TEAQL_FIX_TIME);
347-
return (T) (captured != null ? captured : java.time.LocalDateTime.now());
347+
return (T) (captured != null ? captured : businessTime());
348348
}
349349
// Delegate to subclass or extension for application-defined expressions.
350350
return evaluateExpression(expression, args);

‎teaql-runtime/src/main/java/io/teaql/runtime/TeaQLRuntime.java‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@
1111
import io.teaql.core.businessid.BusinessIdKeyProvider;
1212
import io.teaql.core.businessid.BusinessIdSchemaContributor;
1313
import io.teaql.core.businessid.BusinessIdService;
14+
import io.teaql.core.businessid.BusinessClock;
1415
import io.teaql.runtime.businessid.DefaultBusinessIdService;
1516
import io.teaql.core.reference.RoundTripReferenceCodec;
1617
import io.teaql.core.reference.RoundTripReferenceProvider;
18+
import io.teaql.runtime.businessid.SystemBusinessClock;
1719
import java.util.*;
1820
import java.util.stream.Stream;
1921

@@ -31,6 +33,7 @@ public class TeaQLRuntime {
3133
private final BusinessIdSchemaContributor businessIdSchemaContributor;
3234
private final BusinessIdKeyProvider businessIdKeyProvider;
3335
private final RoundTripReferenceCodec roundTripReferenceCodec;
36+
private final BusinessClock businessClock;
3437
private final Map<String, Checker<?>> checkers = new java.util.concurrent.ConcurrentHashMap<>();
3538
private final List<GeneratedSchemaBootstrap> generatedBootstraps =
3639
new java.util.concurrent.CopyOnWriteArrayList<>();
@@ -51,6 +54,7 @@ private TeaQLRuntime(Builder builder) {
5154
this.roundTripReferenceCodec = builder.roundTripReferenceProvider == null
5255
? null
5356
: new RoundTripReferenceCodec(builder.roundTripReferenceProvider);
57+
this.businessClock = builder.businessClock;
5458
}
5559

5660
public static Builder builder() {
@@ -101,6 +105,10 @@ BusinessIdService getBusinessIdService() {
101105
return businessIdService;
102106
}
103107

108+
BusinessClock getBusinessClock() {
109+
return businessClock;
110+
}
111+
104112
BusinessIdSchemaContributor getBusinessIdSchemaContributor() {
105113
return businessIdSchemaContributor;
106114
}
@@ -524,7 +532,7 @@ private void checkAndFix(UserContext context, Entity entity) {
524532
context.beginFixEvidence();
525533
boolean ownsFixTime = context.getAttribute(Checker.TEAQL_FIX_TIME) == null;
526534
if (ownsFixTime) {
527-
context.putAttribute(Checker.TEAQL_FIX_TIME, java.time.LocalDateTime.now());
535+
context.putAttribute(Checker.TEAQL_FIX_TIME, context.businessTime());
528536
}
529537
try {
530538
checker.checkAndFix(context, (BaseEntity) entity);
@@ -1007,6 +1015,7 @@ public static class Builder {
10071015
private BusinessIdSchemaContributor businessIdSchemaContributor;
10081016
private BusinessIdKeyProvider businessIdKeyProvider;
10091017
private RoundTripReferenceProvider roundTripReferenceProvider;
1018+
private BusinessClock businessClock = SystemBusinessClock.INSTANCE;
10101019

10111020
public Builder metadata(EntityMetaFactory metadata) {
10121021
this.metadata = metadata;
@@ -1045,6 +1054,12 @@ public Builder businessIdService(BusinessIdService businessIdService) {
10451054
return this;
10461055
}
10471056

1057+
/** Installs the time source visible to domain behavior in each context. */
1058+
public Builder businessClock(BusinessClock businessClock) {
1059+
this.businessClock = java.util.Objects.requireNonNull(businessClock, "businessClock");
1060+
return this;
1061+
}
1062+
10481063
/**
10491064
* Installs one provider-backed allocator for generated Fix execution and
10501065
* the explicit {@code context.ensureSchema()} lifecycle.

‎teaql-runtime/src/main/java/io/teaql/runtime/businessid/SystemBusinessClock.java‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import io.teaql.core.UserContext;
44
import io.teaql.core.businessid.BusinessClock;
55
import java.time.LocalDate;
6+
import java.time.LocalDateTime;
67
import java.time.ZoneId;
78

89
/** Host time is isolated behind a context capability and can be replaced in tests. */
@@ -14,8 +15,16 @@ private SystemBusinessClock() {
1415

1516
@Override
1617
public LocalDate businessDate(UserContext context) {
18+
return businessDateTime(context).toLocalDate();
19+
}
20+
21+
@Override
22+
public LocalDateTime businessDateTime(UserContext context) {
23+
return LocalDateTime.now(resolveZone(context));
24+
}
25+
26+
private ZoneId resolveZone(UserContext context) {
1727
Object zone = context.getAttribute("teaql.business.zone");
18-
ZoneId zoneId = zone instanceof ZoneId ? (ZoneId) zone : ZoneId.systemDefault();
19-
return LocalDate.now(zoneId);
28+
return zone instanceof ZoneId ? (ZoneId) zone : ZoneId.systemDefault();
2029
}
2130
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package io.teaql.runtime;
2+
3+
import static org.junit.Assert.assertEquals;
4+
5+
import io.teaql.core.UserContext;
6+
import io.teaql.core.businessid.BusinessClock;
7+
import io.teaql.core.meta.SimpleEntityMetaFactory;
8+
import java.time.LocalDate;
9+
import java.time.LocalDateTime;
10+
import org.junit.Test;
11+
12+
public class BusinessClockTest {
13+
14+
@Test
15+
public void contextUsesTheRuntimeClockForDateTimeAndNowExpression() {
16+
LocalDateTime expected = LocalDateTime.of(2032, 2, 29, 10, 15, 30);
17+
BusinessClock fixedClock = new BusinessClock() {
18+
@Override
19+
public LocalDate businessDate(UserContext context) {
20+
return expected.toLocalDate();
21+
}
22+
23+
@Override
24+
public LocalDateTime businessDateTime(UserContext context) {
25+
return expected;
26+
}
27+
};
28+
TeaQLRuntime runtime = TeaQLRuntime.builder()
29+
.metadata(new SimpleEntityMetaFactory())
30+
.businessClock(fixedClock)
31+
.build();
32+
33+
DefaultUserContext context = new DefaultUserContext(runtime);
34+
35+
assertEquals(expected.toLocalDate(), context.businessDate());
36+
assertEquals(expected, context.businessTime());
37+
assertEquals(expected, context.<LocalDateTime>evaluate("now"));
38+
}
39+
40+
@Test
41+
public void existingDateOnlyClockRemainsSourceCompatibleAndDeterministic() {
42+
LocalDate expected = LocalDate.of(2033, 1, 2);
43+
TeaQLRuntime runtime = TeaQLRuntime.builder()
44+
.metadata(new SimpleEntityMetaFactory())
45+
.businessClock(ignored -> expected)
46+
.build();
47+
48+
DefaultUserContext context = new DefaultUserContext(runtime);
49+
50+
assertEquals(expected.atStartOfDay(), context.businessTime());
51+
}
52+
}

0 commit comments

Comments
 (0)