diff --git a/pages/docs/dsl.md b/pages/docs/dsl.md index 11bdae78d914918e763cb1a947972036c34ca9b8..3f83c719934a1eebb6a35978fe55c4c01f65c9be 100644 --- a/pages/docs/dsl.md +++ b/pages/docs/dsl.md @@ -29,13 +29,53 @@ A breakdown of the parts of that syntax: As described above, it can be combined with `indexed`. If used on its own, the incoming data is interpreted as a complete list and its elements will be appended to the current list. - The `<Non-Terminal>[.<Target>["()"]]` notation describes the actual affected node. - - If the target is omitted, all nodes of that non-terminal type can be connected, irrespective of their context. + - If the target is omitted, all nodes of that non-terminal type can be connected, irrespective of their context. This is a context-free endpoint definition. - The target can be any child on the right-hand side of a production rule, a role of a relation, or an attribute. The brackets `()` after the target must be used in case of an attribute, and only then. - Optionally, an endpoint can use one or more [mappings](#mappings). They will be applied before sending, or after receiving a message. Mappings will always be applied in the order they are listed after `using`. +### Context-Free Endpoints + +!!! attention + Context-Free endpoints are currently only supported for receiving endpoints. + +An endpoint with only a non-terminal and without a target is called context-free endpoint. +Specifying such an endpoint has several consequences: + +- The given non-terminal can be connected to in all contexts it occurs as if there were endpoints for all those contexts. +- There is a special method available on the given non-terminal to connect itself, which selects the correct connect-method depending on its context. +- Context-sensitive endpoints for this non-terminal can still be specified to modify mappings in this context. If the context is a list, the endpoint must use `indexed` and cannot use `with add`. + +**Example**: + +```java +// grammar +Root ::= A SingleA:A [OptA:A] ListA:A* ; +A ::= <Value> ; + +// connect +receive A; +receive Root.SingleA using MyMapping; // specialized endpoint +``` + +Implied, additional connect specifications: +```java +receive Root.A; +receive Root.OptA; +receive indexed Root.ListA; +``` + +Application code: +```java +A a = root.getOptA(); +// new method on A: +a.connect("<some-uri-to-connect>"); +// equivalent to (implicitly generated): +root.connectOptA("<some-uri-to-connect>"); +``` + ## Mappings A mapping is a side effect-free function with one argument (the value that will be transformed) and one result (the transformed value), that will be applied on a value to be sent for a sending endpoint, a received value for a receiving endpoint, or the result of another mapping. diff --git a/ragconnect.base/src/main/jastadd/Analysis.jrag b/ragconnect.base/src/main/jastadd/Analysis.jrag index 66e6a5a81aff0823c07e4ec3079551c0740b4963..5659c9321a0016162ee5ca9715d6a38770c2a693 100644 --- a/ragconnect.base/src/main/jastadd/Analysis.jrag +++ b/ragconnect.base/src/main/jastadd/Analysis.jrag @@ -12,6 +12,11 @@ aspect Analysis { .filter(containingEndpointDefinition()::matchesType) .count() > 1; } + eq ContextFreeTypeEndpointTarget.isAlreadyDefined() { + return lookupContextFreeTypeEndpointDefinitions(getTypeDecl()).stream() + .filter(containingEndpointDefinition()::matchesType) + .count() > 1; + } syn boolean DependencyDefinition.isAlreadyDefined() = lookupDependencyDefinition(getSource().containingTypeDecl(), getID()) != this; // --- matchesType --- @@ -61,6 +66,7 @@ aspect Analysis { syn boolean EndpointTarget.entityIsNormalAttribute(); eq TokenEndpointTarget.entityIsNormalAttribute() = !getToken().getNTA(); eq TypeEndpointTarget.entityIsNormalAttribute() = !getType().getNTA(); + eq ContextFreeTypeEndpointTarget.entityIsNormalAttribute() = false; // --- needProxyToken --- syn boolean TokenComponent.needProxyToken() = !getDependencySourceDefinitionList().isEmpty() || getTokenEndpointTargetList().stream().map(EndpointTarget::containingEndpointDefinition).anyMatch(EndpointDefinition::shouldSendValue); diff --git a/ragconnect.base/src/main/jastadd/Errors.jrag b/ragconnect.base/src/main/jastadd/Errors.jrag index 61f966507a1d793ad711bb0e3dde085ee0b46ed6..f1924e72f4b5944ee8897390f3db494bfd48c4c9 100644 --- a/ragconnect.base/src/main/jastadd/Errors.jrag +++ b/ragconnect.base/src/main/jastadd/Errors.jrag @@ -27,6 +27,16 @@ aspect Errors { token().effectiveJavaTypeUse()) to RagConnect.errors(); + ContextFreeTypeEndpointTarget contributes error("Context-Free endpoint not allowed for root node " + + getTypeDecl().getName() + "!") + when getTypeDecl().occurencesInProductionRules().isEmpty() + to RagConnect.errors(); + + EndpointDefinition contributes error("Clash with implied, indexed endpoint definition of context-free endpoint in line " + + clashingContextFreeEndpointDefinition().getStartLine() + "!") + when !getSend() && clashingContextFreeEndpointDefinition() != null + to RagConnect.errors(); + DependencyDefinition contributes error("Dependency definition already defined for " + getSource().containingTypeDecl().getName() + " with name " + getID()) when isAlreadyDefined() to RagConnect.errors(); @@ -45,6 +55,18 @@ aspect ErrorHelpers { } return false; } + + syn EndpointDefinition EndpointDefinition.clashingContextFreeEndpointDefinition() { + if (getSend() || !typeIsList() || getIndexBasedListAccess()) { + return null; + } + List<EndpointDefinition> contextFreeEndpointsWithSameType = lookupContextFreeTypeEndpointDefinitions( + getEndpointTarget().asTypeEndpointTarget().getType().getTypeDecl()); + if (!contextFreeEndpointsWithSameType.isEmpty()) { + return contextFreeEndpointsWithSameType.get(0); + } + return null; + } } aspect ErrorMessage { diff --git a/ragconnect.base/src/main/jastadd/Intermediate.jadd b/ragconnect.base/src/main/jastadd/Intermediate.jadd index 3729453a4123d34514ad3475b439139bd7ef7136..a391130b5dd5991f1ceba4c0d2688a8bb17015a3 100644 --- a/ragconnect.base/src/main/jastadd/Intermediate.jadd +++ b/ragconnect.base/src/main/jastadd/Intermediate.jadd @@ -157,6 +157,14 @@ aspect MustacheMappingApplicationAndDefinition { eq MTypeSendDefinition.preemptiveExpectedValue() = lastValue(); eq MTypeSendDefinition.preemptiveReturn() = "return false;"; + eq MContextFreeTypeReceiveDefinition.firstInputVarName() = "message"; + eq MContextFreeTypeReceiveDefinition.preemptiveExpectedValue() = "this"; + eq MContextFreeTypeReceiveDefinition.preemptiveReturn() = "return;"; + + eq MContextFreeTypeSendDefinition.firstInputVarName() = null; + eq MContextFreeTypeSendDefinition.preemptiveExpectedValue() = null; + eq MContextFreeTypeSendDefinition.preemptiveReturn() = null; + syn String MEndpointDefinition.parentTypeName() = getEndpointDefinition().parentTypeName(); syn String MEndpointDefinition.getterMethodName() = getEndpointDefinition().getterMethodName(); @@ -174,9 +182,17 @@ aspect MustacheRagConnect { syn List<EndpointDefinition> RagConnect.allEndpointDefinitionList() { List<EndpointDefinition> result = new ArrayList<>(); - for (ConnectSpecification spec : getConnectSpecificationFileList()) { - spec.getEndpointDefinitionList().forEach(result::add); + // first gather all user-defined endpoint definitions, that are not context-free + for (EndpointDefinition def : givenEndpointDefinitionList()) { + if (!def.hasContextFreeTypeEndpointTarget()) { + result.add(def); + } + } + // then check for additional endpoints, and add if no conflict with existing definitions exists + for (EndpointDefinition def : givenEndpointDefinitionList()) { + def.getEndpointTarget().impliedEndpointDefinitions().iterator().forEachRemaining(result::add); } + return result; } @@ -192,8 +208,54 @@ aspect MustacheRagConnect { return result; } + syn List<TypeDecl> RagConnect.typeDeclsOfContextFreeEndpointTargets() { + List<TypeDecl> result = new ArrayList<>(); + for (EndpointTarget target : givenEndpointTargetList()) { + if (target.isContextFreeTypeEndpointTarget()) { + result.add(target.asContextFreeTypeEndpointTarget().getTypeDecl()); + } + } + return result; + } + // === MappingDefinition === syn boolean MappingDefinition.isUsed() = !effectiveUsedAt().isEmpty(); + + // === attributes needed for computing above ones === + syn List<EndpointDefinition> RagConnect.givenEndpointDefinitionList() { + List<EndpointDefinition> result = new ArrayList<>(); + for (ConnectSpecification spec : getConnectSpecificationFileList()) { + spec.getEndpointDefinitionList().forEach(result::add); + } + return result; + } + + syn nta JastAddList<EndpointDefinition> EndpointTarget.impliedEndpointDefinitions() = new JastAddList<>(); + eq ContextFreeTypeEndpointTarget.impliedEndpointDefinitions() { + JastAddList<EndpointDefinition> result = super.impliedEndpointDefinitions(); + EndpointDefinition containingDef = containingEndpointDefinition(); + for (TypeComponent typeComponent : getTypeDecl().occurencesInProductionRules()) { + List<EndpointDefinition> defsForTypeComponent = lookupTypeEndpointDefinitions(typeComponent); + if (!defsForTypeComponent.stream().anyMatch(containingDef::matchesType)) { + // there is no user-defined endpoint definition for this typeComponent yet + // -> create a new endpoint definition with the same options and mappings as the context-free def + // (except indexed-based for list-types) + EndpointDefinition newDef = new EndpointDefinition(); + newDef.setAlwaysApply(containingDef.getAlwaysApply()); + newDef.setIndexBasedListAccess(typeComponent.isListComponent()); + newDef.setSend(containingDef.getSend()); + containingDef.getMappings().forEach(newDef::addMapping); + + TypeEndpointTarget target = new TypeEndpointTarget(); + target.setType(typeComponent); + newDef.setEndpointTarget(target); + + result.add(newDef); + } + } + return result; + } + } aspect MustacheReceiveAndSendAndHandleUri { @@ -234,6 +296,10 @@ aspect MustacheReceiveAndSendAndHandleUri { eq TypeEndpointTarget.parentTypeName() = getType().containingTypeDecl().getName(); eq TypeEndpointTarget.entityName() = getType().getName() + (typeIsList() && !containingEndpointDefinition().getIndexBasedListAccess() ? "List" : ""); + eq ContextFreeTypeEndpointTarget.getterMethodName() = null; + eq ContextFreeTypeEndpointTarget.parentTypeName() = getTypeDecl().getName(); + eq ContextFreeTypeEndpointTarget.entityName() = ""; + } aspect MustacheReceiveDefinition { @@ -241,6 +307,8 @@ aspect MustacheReceiveDefinition { syn boolean RagConnect.configLoggingEnabledForReads() = getConfiguration().getLoggingEnabledForReads(); // === EndpointDefinition === + syn boolean EndpointDefinition.hasContextFreeTypeEndpointTarget() = getEndpointTarget().isContextFreeTypeEndpointTarget(); + syn boolean EndpointDefinition.hasTypeEndpointTarget() = getEndpointTarget().isTypeEndpointTarget(); syn String EndpointDefinition.idTokenName() = "InternalRagconnectTopicInList"; @@ -281,26 +349,29 @@ aspect MustacheSendDefinition { syn String EndpointTarget.senderName(); eq TokenEndpointTarget.senderName() = "_sender_" + getToken().getName(); eq TypeEndpointTarget.senderName() = "_sender_" + getType().getName(); + eq ContextFreeTypeEndpointTarget.senderName() = null; syn String MEndpointDefinition.updateMethodName(); syn String MEndpointDefinition.writeMethodName(); - // MTokenReceiveDefinition eq MTokenReceiveDefinition.updateMethodName() = null; eq MTokenReceiveDefinition.writeMethodName() = null; - // MTokenSendDefinition eq MTokenSendDefinition.updateMethodName() = "_update_" + tokenName(); eq MTokenSendDefinition.writeMethodName() = "_writeLastValue_" + tokenName(); - // MTypeReceiveDefinition eq MTypeReceiveDefinition.updateMethodName() = null; eq MTypeReceiveDefinition.writeMethodName() = null; - // MTypeSendDefinition eq MTypeSendDefinition.updateMethodName() = "_update_" + typeName(); eq MTypeSendDefinition.writeMethodName() = "_writeLastValue_" + typeName(); + eq MContextFreeTypeReceiveDefinition.updateMethodName() = null; + eq MContextFreeTypeReceiveDefinition.writeMethodName() = null; + + eq MContextFreeTypeSendDefinition.updateMethodName() = null; + eq MContextFreeTypeSendDefinition.writeMethodName() = null; + syn String EndpointDefinition.tokenName() = token().getName(); syn String MEndpointDefinition.tokenName() = getEndpointDefinition().tokenName(); @@ -350,6 +421,25 @@ aspect MustacheTokenComponent { // > see MustacheSend for updateMethodName, writeMethodName } +aspect MustacheTypeDecl { + // === TypeComponent === + syn String TypeComponent.parentTypeName() = containingTypeDecl().getName(); + + // === TypeDecl === + syn List<TypeComponent> TypeDecl.occurencesInProductionRules() { + List<TypeComponent> result = new ArrayList<>(); + for (TypeDecl typeDecl : program().typeDecls()) { + for (Component comp : typeDecl.getComponentList()) { + if (comp.isTypeComponent() && comp.asTypeComponent().getTypeDecl().equals(this)) { + result.add(comp.asTypeComponent()); + } + } + } + return result; + } + +} + aspect AttributesForMustache { syn String MEndpointDefinition.lastValue() = getEndpointDefinition().lastValue(); @@ -361,22 +451,7 @@ aspect AttributesForMustache { syn MInnerMappingDefinition MEndpointDefinition.lastDefinition() = getInnerMappingDefinition(getNumInnerMappingDefinition() - 1); syn nta MEndpointDefinition EndpointDefinition.toMustache() { - final MEndpointDefinition result; - if (getEndpointTarget().isTokenEndpointTarget()) { - if (getSend()) { - result = new MTokenSendDefinition(); - } else { - result = new MTokenReceiveDefinition(); - } - } else if (getEndpointTarget().isTypeEndpointTarget()) { - if (getSend()) { - result = new MTypeSendDefinition(); - } else { - result = new MTypeReceiveDefinition(); - } - } else { - throw new RuntimeException("Unknown endpoint target type: " + getEndpointTarget()); - } + final MEndpointDefinition result = getEndpointTarget().createMEndpointDefinition(getSend()); result.setEndpointDefinition(this); for (MappingDefinition def : effectiveMappings()) { MInnerMappingDefinition inner = new MInnerMappingDefinition(); @@ -385,6 +460,20 @@ aspect AttributesForMustache { } return result; } + abstract MEndpointDefinition EndpointTarget.createMEndpointDefinition(boolean isSend); + MEndpointDefinition TokenEndpointTarget.createMEndpointDefinition(boolean isSend) { + return isSend ? new MTokenSendDefinition() : new MTokenReceiveDefinition(); + } + MEndpointDefinition TypeEndpointTarget.createMEndpointDefinition(boolean isSend) { + return isSend ? new MTypeSendDefinition() : new MTypeReceiveDefinition(); + } + MEndpointDefinition ContextFreeTypeEndpointTarget.createMEndpointDefinition(boolean isSend) { + return isSend ? new MContextFreeTypeSendDefinition() : new MContextFreeTypeReceiveDefinition(); + } + MEndpointDefinition UntypedEndpointTarget.createMEndpointDefinition(boolean isSend) { + throw new RuntimeException("Untyped endpoint target type, typeName= " + + getTypeName() + ", childName=" + getChildName()); + } } aspect GrammarGeneration { diff --git a/ragconnect.base/src/main/jastadd/Intermediate.relast b/ragconnect.base/src/main/jastadd/Intermediate.relast index d7afd2ea942558f6999c8fa64c91bfdc480d659c..79097503ef32b417be179ccb5f679886a6297582 100644 --- a/ragconnect.base/src/main/jastadd/Intermediate.relast +++ b/ragconnect.base/src/main/jastadd/Intermediate.relast @@ -7,6 +7,9 @@ MTokenSendDefinition : MTokenEndpointDefinition; abstract MTypeEndpointDefinition : MEndpointDefinition; MTypeReceiveDefinition : MTypeEndpointDefinition; MTypeSendDefinition : MTypeEndpointDefinition; +abstract MContextFreeTypeEndpointDefinition : MEndpointDefinition; +MContextFreeTypeReceiveDefinition : MContextFreeTypeEndpointDefinition; +MContextFreeTypeSendDefinition : MContextFreeTypeEndpointDefinition; MInnerMappingDefinition; rel MInnerMappingDefinition.MappingDefinition -> MappingDefinition; diff --git a/ragconnect.base/src/main/jastadd/Mappings.jrag b/ragconnect.base/src/main/jastadd/Mappings.jrag index 093f4cc1d4f1cb0eecf1ffe7914c20e6fa715b27..45d28e8d99c02292780fed005642e72489a1c58e 100644 --- a/ragconnect.base/src/main/jastadd/Mappings.jrag +++ b/ragconnect.base/src/main/jastadd/Mappings.jrag @@ -294,6 +294,7 @@ aspect Mappings { syn String EndpointTarget.targetTypeName(); eq TokenEndpointTarget.targetTypeName() = getToken().effectiveJavaTypeUse().getName(); eq TypeEndpointTarget.targetTypeName() = getType().getTypeDecl().getName(); + eq ContextFreeTypeEndpointTarget.targetTypeName() = getTypeDecl().getName(); // eq ReceiveFromRestDefinition.suitableDefaultMapping() { // String typeName = getMappingList().isEmpty() ? diff --git a/ragconnect.base/src/main/jastadd/NameResolution.jrag b/ragconnect.base/src/main/jastadd/NameResolution.jrag index 9b37ee7f93ea9f31414f61a3a70b01a1ba5ca489..cf29d470e5734409b9cc8debd48ef70c3e281638 100644 --- a/ragconnect.base/src/main/jastadd/NameResolution.jrag +++ b/ragconnect.base/src/main/jastadd/NameResolution.jrag @@ -5,7 +5,7 @@ aspect RagConnectNameResolution { eq RagConnect.getConnectSpecificationFile().lookupTokenEndpointDefinitions(TokenComponent token) = lookupTokenEndpointDefinitions(token); syn java.util.List<EndpointDefinition> RagConnect.lookupTokenEndpointDefinitions(TokenComponent token) { java.util.List<EndpointDefinition> result = new java.util.ArrayList<>(); - for (EndpointTarget target : allEndpointTargetList()) { + for (EndpointTarget target : givenEndpointTargetList()) { if (target.isTokenEndpointTarget() && target.asTokenEndpointTarget().getToken().equals(token)) { result.add(target.containingEndpointDefinition()); } @@ -19,7 +19,7 @@ aspect RagConnectNameResolution { eq RagConnect.getConnectSpecificationFile().lookupTypeEndpointDefinitions(TypeComponent type) = lookupTypeEndpointDefinitions(type); syn java.util.List<EndpointDefinition> RagConnect.lookupTypeEndpointDefinitions(TypeComponent type) { java.util.List<EndpointDefinition> result = new java.util.ArrayList<>(); - for (EndpointTarget target : allEndpointTargetList()) { + for (EndpointTarget target : givenEndpointTargetList()) { if (target.isTypeEndpointTarget() && target.asTypeEndpointTarget().getType().equals(type)) { result.add(target.containingEndpointDefinition()); } @@ -27,6 +27,20 @@ aspect RagConnectNameResolution { return result; } + // --- lookupContextFreeTypeEndpointDefinition --- + inh java.util.List<EndpointDefinition> EndpointDefinition.lookupContextFreeTypeEndpointDefinitions(TypeDecl typeDecl); + inh java.util.List<EndpointDefinition> EndpointTarget.lookupContextFreeTypeEndpointDefinitions(TypeDecl typeDecl); + eq RagConnect.getConnectSpecificationFile().lookupContextFreeTypeEndpointDefinitions(TypeDecl typeDecl) = lookupContextFreeTypeEndpointDefinitions(typeDecl); + syn java.util.List<EndpointDefinition> RagConnect.lookupContextFreeTypeEndpointDefinitions(TypeDecl typeDecl) { + java.util.List<EndpointDefinition> result = new java.util.ArrayList<>(); + for (EndpointTarget target : givenEndpointTargetList()) { + if (target.isContextFreeTypeEndpointTarget() && target.asContextFreeTypeEndpointTarget().getTypeDecl().equals(typeDecl)) { + result.add(target.containingEndpointDefinition()); + } + } + return result; + } + // --- lookupDependencyDefinition --- inh DependencyDefinition DependencyDefinition.lookupDependencyDefinition(TypeDecl source, String id); eq RagConnect.getConnectSpecificationFile().lookupDependencyDefinition(TypeDecl source, String id) { diff --git a/ragconnect.base/src/main/jastadd/Navigation.jrag b/ragconnect.base/src/main/jastadd/Navigation.jrag index 7419f3d880b592e28ae4a45b7a9451849b855368..a65ab72fa946bcec3351bb496f6632c012bac678 100644 --- a/ragconnect.base/src/main/jastadd/Navigation.jrag +++ b/ragconnect.base/src/main/jastadd/Navigation.jrag @@ -12,6 +12,12 @@ aspect NewStuff { syn boolean EndpointTarget.isTypeEndpointTarget() = false; eq TypeEndpointTarget.isTypeEndpointTarget() = true; + /** Tests if EndpointTarget is a ContextFreeTypeEndpointTarget. + * @return 'true' if this is a ContextFreeTypeEndpointTarget, otherwise 'false' + */ + syn boolean EndpointTarget.isContextFreeTypeEndpointTarget() = false; + eq ContextFreeTypeEndpointTarget.isContextFreeTypeEndpointTarget() = true; + /** Tests if EndpointTarget is a UntypedEndpointTarget. * @return 'true' if this is a UntypedEndpointTarget, otherwise 'false' */ @@ -32,6 +38,13 @@ aspect NewStuff { eq EndpointTarget.asTypeEndpointTarget() = null; eq TypeEndpointTarget.asTypeEndpointTarget() = this; + /** casts a EndpointTarget into a ContextFreeTypeEndpointTarget if possible. + * @return 'this' cast to a ContextFreeTypeEndpointTarget or 'null' + */ + syn ContextFreeTypeEndpointTarget EndpointTarget.asContextFreeTypeEndpointTarget(); + eq EndpointTarget.asContextFreeTypeEndpointTarget() = null; + eq ContextFreeTypeEndpointTarget.asContextFreeTypeEndpointTarget() = this; + /** casts a EndpointTarget into a UntypedEndpointTarget if possible. * @return 'this' cast to a UntypedEndpointTarget or 'null' */ @@ -73,8 +86,8 @@ aspect RagConnectNavigation { } - //--- allEndpointTargetList --- - syn List<EndpointTarget> RagConnect.allEndpointTargetList() { + //--- givenEndpointTargetList --- + syn List<EndpointTarget> RagConnect.givenEndpointTargetList() { List<EndpointTarget> result = new ArrayList<>(); for (ConnectSpecification spec : getConnectSpecificationFileList()) { spec.getEndpointDefinitionList().forEach(endpointDef -> result.add(endpointDef.getEndpointTarget())); diff --git a/ragconnect.base/src/main/jastadd/RagConnect.relast b/ragconnect.base/src/main/jastadd/RagConnect.relast index 30e6d5ad6a034f9b35de570414e8d4e904751ca2..37ec5feaecbff82163d7ca1360c9916abfaed36e 100644 --- a/ragconnect.base/src/main/jastadd/RagConnect.relast +++ b/ragconnect.base/src/main/jastadd/RagConnect.relast @@ -11,7 +11,9 @@ TokenEndpointTarget : EndpointTarget; rel TokenEndpointTarget.Token <-> TokenComponent.TokenEndpointTarget*; TypeEndpointTarget : EndpointTarget; rel TypeEndpointTarget.Type <-> TypeComponent.TypeEndpointTarget*; -UntypedEndpointTarget : EndpointTarget ::= <TokenOrType>; // only used by parser +ContextFreeTypeEndpointTarget : EndpointTarget; +rel ContextFreeTypeEndpointTarget.TypeDecl <-> TypeDecl.ContextFreeTypeEndpointTarget?; +UntypedEndpointTarget : EndpointTarget ::= <TypeName> <ChildName>; // only used by parser // to be integrated: //AttributeEndpointTarget : EndpointTarget ::= <Name> ; //RelationEndpointTarget : EndpointTarget ; diff --git a/ragconnect.base/src/main/jastadd/parser/ParserRewrites.jrag b/ragconnect.base/src/main/jastadd/parser/ParserRewrites.jrag index bbf0c11b7ba08c1f7c172cf8fd31420e7fd74561..23dbabaef4ce3d4e0ce114591d1c4ee61937b878 100644 --- a/ragconnect.base/src/main/jastadd/parser/ParserRewrites.jrag +++ b/ragconnect.base/src/main/jastadd/parser/ParserRewrites.jrag @@ -1,19 +1,37 @@ aspect ParserRewrites { rewrite UntypedEndpointTarget { - when (tryGloballyResolveTypeComponentByToken(getTokenOrType()) != null) + when (getChildName() != null && tryGloballyResolveTypeComponentByToken(combinedName()) != null) to TypeEndpointTarget { TypeEndpointTarget result = new TypeEndpointTarget(); - result.setType(TypeComponent.createRef(this.getTokenOrType())); + result.copyOtherValuesFrom(this); + result.setType(TypeComponent.createRef(this.combinedName())); return result; } - when (tryGloballyResolveTokenComponentByToken(getTokenOrType()) != null) + + when (getChildName() != null && tryGloballyResolveTokenComponentByToken(combinedName()) != null) to TokenEndpointTarget { TokenEndpointTarget result = new TokenEndpointTarget(); - result.setToken(TokenComponent.createRef(this.getTokenOrType())); + result.copyOtherValuesFrom(this); + result.setToken(TokenComponent.createRef(this.combinedName())); + return result; + } + + when (getChildName() == "") + to ContextFreeTypeEndpointTarget { + ContextFreeTypeEndpointTarget result = new ContextFreeTypeEndpointTarget(); + result.copyOtherValuesFrom(this); + result.setTypeDecl(TypeDecl.createRef(getTypeName())); return result; } } + syn String UntypedEndpointTarget.combinedName() = getTypeName() + "." + getChildName(); + + protected void EndpointTarget.copyOtherValuesFrom(EndpointTarget source) { + this.setStart(source.getStartLine(), source.getStartColumn()); + this.setEnd(source.getEndLine(), source.getEndColumn()); + } + eq UntypedEndpointTarget.senderName() = "<untyped.senderName>"; eq UntypedEndpointTarget.getterMethodName() = "<untyped.getterMethodName>"; eq UntypedEndpointTarget.parentTypeName() = "<untyped.parentTypeName>"; diff --git a/ragconnect.base/src/main/jastadd/parser/RagConnect.parser b/ragconnect.base/src/main/jastadd/parser/RagConnect.parser index 5ddef630253006a13e64ea84cd4df30dc4b6aa2b..2c326763856f437e770e9bf5ba1566c86f2a2fdb 100644 --- a/ragconnect.base/src/main/jastadd/parser/RagConnect.parser +++ b/ragconnect.base/src/main/jastadd/parser/RagConnect.parser @@ -27,13 +27,13 @@ ConnectSpecificationFile connect_specification_file // return def; // } private EndpointDefinition createEndpointDefinition( - String type_name, String child_name, boolean send, + EndpointTarget endpointTarget, boolean send, boolean indexBasedListAccess, boolean withAdd) { EndpointDefinition result = new EndpointDefinition(); result.setSend(send); result.setIndexBasedListAccess(indexBasedListAccess); result.setWithAdd(withAdd); - result.setEndpointTarget(new UntypedEndpointTarget(type_name + "." + child_name)); + result.setEndpointTarget(endpointTarget); return result; } :} ; @@ -53,30 +53,16 @@ EndpointDefinition endpoint_definition ; EndpointDefinition endpoint_definition_type - = SEND ID.type_name DOT ID.child_name - {: - return createEndpointDefinition(type_name, child_name, true, false, false); - :} -// | SEND INDEXED ID.type_name DOT ID.child_name -// {: -// return createEndpointDefinition(type_name, child_name, true, true, false); -// :} - | RECEIVE ID.type_name DOT ID.child_name - {: - return createEndpointDefinition(type_name, child_name, false, false, false); - :} - | RECEIVE INDEXED ID.type_name DOT ID.child_name - {: - return createEndpointDefinition(type_name, child_name, false, true, false); - :} - | RECEIVE WITH ADD ID.type_name DOT ID.child_name - {: - return createEndpointDefinition(type_name, child_name, false, false, true); - :} - | RECEIVE INDEXED WITH ADD ID.type_name DOT ID.child_name - {: - return createEndpointDefinition(type_name, child_name, false, true, true); - :} + = SEND endpoint_target.t {: return createEndpointDefinition(t, true, false, false); :} + | RECEIVE endpoint_target.t {: return createEndpointDefinition(t, false, false, false); :} + | RECEIVE INDEXED endpoint_target.t {: return createEndpointDefinition(t, false, true, false); :} + | RECEIVE WITH ADD endpoint_target.t {: return createEndpointDefinition(t, false, false, true ); :} + | RECEIVE INDEXED WITH ADD endpoint_target.t {: return createEndpointDefinition(t, false, true, true ); :} +; + +EndpointTarget endpoint_target + = ID.type_name DOT ID.child_name {: return new UntypedEndpointTarget(type_name, child_name); :} + | ID.type_name {: return new UntypedEndpointTarget(type_name, ""); :} ; ArrayList string_list diff --git a/ragconnect.base/src/main/resources/ragconnect.mustache b/ragconnect.base/src/main/resources/ragconnect.mustache index c3b50710f38fd247aca8ca11456c23279a0bee8c..d39fe4a8686b7a6950c1bfb7128d0960f8fa8349 100644 --- a/ragconnect.base/src/main/resources/ragconnect.mustache +++ b/ragconnect.base/src/main/resources/ragconnect.mustache @@ -28,6 +28,10 @@ aspect RagConnect { {{> tokenComponent}} {{/tokenComponentsThatNeedProxy}} + {{#typeDeclsOfContextFreeEndpointTargets}} + {{> typeDecl}} + {{/typeDeclsOfContextFreeEndpointTargets}} + {{> ListAspect}} public void {{rootNodeName}}.ragconnectCheckIncremental() { diff --git a/ragconnect.base/src/main/resources/typeDecl.mustache b/ragconnect.base/src/main/resources/typeDecl.mustache new file mode 100644 index 0000000000000000000000000000000000000000..643fbc80a4eccfa50da5d48650e03d4380badb62 --- /dev/null +++ b/ragconnect.base/src/main/resources/typeDecl.mustache @@ -0,0 +1,45 @@ +uncache {{Name}}._ragconnect_myContext(); +inh String {{Name}}._ragconnect_myContext(); +inh ASTNode {{Name}}._ragconnect_myParent(); +uncache {{Name}}._ragconnect_myIndexInList(); +inh int {{Name}}._ragconnect_myIndexInList(); +{{#occurencesInProductionRules}} + eq {{parentTypeName}}.get{{Name}}()._ragconnect_myContext() = "{{parentTypeName}}.{{Name}}"; + eq {{parentTypeName}}.get{{Name}}()._ragconnect_myParent() = this; + {{^isListComponent}} + eq {{parentTypeName}}.get{{Name}}()._ragconnect_myIndexInList() = -1; + {{/isListComponent}} + {{#isListComponent}} + eq {{parentTypeName}}.get{{Name}}(int i)._ragconnect_myIndexInList() = i; + {{/isListComponent}} +{{/occurencesInProductionRules}} + +{{#ContextFreeTypeEndpointTarget}}{{#containingEndpointDefinition}} +public boolean {{Name}}.{{connectMethodName}}(String {{connectParameterName}}) throws java.io.IOException { + switch (_ragconnect_myContext()) { + {{#occurencesInProductionRules}} + {{!only using "connectMethodName" is not correct, since the actual name might be different, e.g., if both send and receive are defined. need a reference to the actual endpoint-definition here}} + {{^isListComponent}} + case "{{parentTypeName}}.{{Name}}": return (({{parentTypeName}}) _ragconnect_myParent()).{{connectMethodName}}{{Name}}({{connectParameterName}}); + {{/isListComponent}} + {{#isListComponent}} + case "{{parentTypeName}}.{{Name}}": return (({{parentTypeName}}) _ragconnect_myParent()).{{connectMethodName}}{{Name}}({{connectParameterName}}, _ragconnect_myIndexInList()); + {{/isListComponent}} + {{/occurencesInProductionRules}} + default: + System.err.println("No matching context while connecting " + this + " to " + {{connectParameterName}}); + return false; + } +} + +public boolean {{Name}}.{{disconnectMethodName}}(String {{connectParameterName}}) throws java.io.IOException { + switch (_ragconnect_myContext()) { +{{#occurencesInProductionRules}} + case "{{parentTypeName}}.{{Name}}": return (({{parentTypeName}}) _ragconnect_myParent()).{{disconnectMethodName}}{{Name}}({{connectParameterName}}); +{{/occurencesInProductionRules}} + default: + System.err.println("No matching context while disconnecting for " + this + " from " + {{connectParameterName}}); + return false; + } +} +{{/containingEndpointDefinition}}{{/ContextFreeTypeEndpointTarget}} diff --git a/ragconnect.tests/build.gradle b/ragconnect.tests/build.gradle index cfeb6ae4680eca67212c256e3e39debc514609c7..865abd9e99d657441dde62ba38112f0e3b18604f 100644 --- a/ragconnect.tests/build.gradle +++ b/ragconnect.tests/build.gradle @@ -105,13 +105,22 @@ task specificTest(type: Test, dependsOn: testClasses) { description = 'Run test tagged with tag given by "-PincludeTags="' group = 'verification' String tags = project.hasProperty("includeTags") ? - project.property("includeTags") : '' + project.property("includeTags") : '' useJUnitPlatform { includeTags tags } } +task newTests(type: Test, dependsOn: testClasses) { + description = 'Run test tagged with tag "New"' + group = 'verification' + + useJUnitPlatform { + includeTags 'New' + } +} + preprocessorTesting { //noinspection GroovyAssignabilityCheck relastCompilerLocation = '../libs/relast.jar' @@ -556,21 +565,27 @@ task compileSingleListVariantIncremental(type: RagConnectTest, dependsOn: ':ragc } } -task cleanCurrentManualTest(type: Delete) { -// delete "src/test/02-after-ragconnect/singleListVariant" -// delete "src/test/03-after-relast/singleListVariant" -// delete "src/test/java-gen/singleListVariant/ast" - delete "src/test/02-after-ragconnect/singleList" - delete "src/test/03-after-relast/singleList" - delete "src/test/java-gen/singleList/ast" -} -task cleanCurrentIncrementalTest(type: Delete) { -// delete "src/test/02-after-ragconnect/singleListVariantInc" -// delete "src/test/03-after-relast/singleListVariantInc" -// delete "src/test/java-gen/singleListVariantInc/ast" - delete "src/test/02-after-ragconnect/singleListInc" - delete "src/test/03-after-relast/singleListInc" - delete "src/test/java-gen/singleListInc/ast" +// --- Test: contextFreeSimple-incremental --- +task compileContextFreeSimpleIncremental(type: RagConnectTest, dependsOn: ':ragconnect.base:jar') { + ragconnect { + outputDir = file('src/test/02-after-ragconnect/contextFreeSimpleInc') + inputFiles = [file('src/test/01-input/contextFreeSimple/Test.relast'), + file('src/test/01-input/contextFreeSimple/Test.connect')] + rootNode = 'Root' + extraOptions = ['--experimental-jastadd-329'] + } + relast { + useJastAddNames = true + grammarName = 'src/test/03-after-relast/contextFreeSimpleInc/contextFreeSimpleInc' + serializer = 'jackson' + } + jastadd { + jastAddList = 'JastAddList' + packageName = 'contextFreeSimpleInc.ast' + extraOptions = ['--tracing=cache,flush', + '--incremental=param', + '--cache=all', + '--rewrite=cnta', + '--flush=full'] + } } -compileSingleListManual.dependsOn cleanCurrentManualTest -compileSingleListIncremental.dependsOn cleanCurrentIncrementalTest diff --git a/ragconnect.tests/src/test/01-input/contextFreeSimple/README.md b/ragconnect.tests/src/test/01-input/contextFreeSimple/README.md new file mode 100644 index 0000000000000000000000000000000000000000..737ca9af132d047a1cceafee2e6b1d2a0552a6b7 --- /dev/null +++ b/ragconnect.tests/src/test/01-input/contextFreeSimple/README.md @@ -0,0 +1,3 @@ +# ContextFree-Simple + +Idea: Use only context-free context, in simple situation (non-recursive) diff --git a/ragconnect.tests/src/test/01-input/contextFreeSimple/Test.connect b/ragconnect.tests/src/test/01-input/contextFreeSimple/Test.connect new file mode 100644 index 0000000000000000000000000000000000000000..7a6aeab676953fdba30bb04f111c7b1fd6bc0655 --- /dev/null +++ b/ragconnect.tests/src/test/01-input/contextFreeSimple/Test.connect @@ -0,0 +1,18 @@ +receive A; + +//"receive Root.A;" is implied +receive Root.SingleA using PrependPrefix; +receive Root.OptA using AddSuffix; +//"receive Root.ListA;" would clash as "receive indexed Root.ListA" is implied + +PrependPrefix maps A a to A {: + A result = new A(); + result.setValue("pre" + a.getValue()); + return result; +:} + +AddSuffix maps A a to A {: + A result = new A(); + result.setValue(a.getValue() + "post"); + return result; +:} diff --git a/ragconnect.tests/src/test/01-input/contextFreeSimple/Test.relast b/ragconnect.tests/src/test/01-input/contextFreeSimple/Test.relast new file mode 100644 index 0000000000000000000000000000000000000000..f5dff3f8b742bc18cfbad715b1326ce483ce8a25 --- /dev/null +++ b/ragconnect.tests/src/test/01-input/contextFreeSimple/Test.relast @@ -0,0 +1,2 @@ +Root ::= A SingleA:A [OptA:A] ListA:A* ; +A ::= <Value> ; diff --git a/ragconnect.tests/src/test/01-input/errors/Errors.relast b/ragconnect.tests/src/test/01-input/errors/Errors.relast index fcde7df1d2e48b9591ddea31aaa3b0a9efdd4cfa..f700f39abdcac18174e192bfc6098d95e4720573 100644 --- a/ragconnect.tests/src/test/01-input/errors/Errors.relast +++ b/ragconnect.tests/src/test/01-input/errors/Errors.relast @@ -1,10 +1,10 @@ -A ::= B C D ; +A ::= B C D E* ; // read definitions B ::= /<ErrorNTA:String>/ <ErrorTypeOfFirstMapping:String> <ErrorTypeOfLastMapping:String> <DoubledValue:int> <ErrorTypeMismatch:String> ; // write definitions -C ::= <ErrorNotNTA:String> /<ErrorTypeOfFirstMapping:String>/ /<ErrorTypeOfLastMapping1:String>/ /<ErrorTypeOfLastMapping2:List<String>>/ /<ErrorTypeMismatch:String>/ /<DoubledValue:int>/ ; +C ::= /<ErrorTypeOfFirstMapping:String>/ /<ErrorTypeOfLastMapping1:String>/ /<ErrorTypeOfLastMapping2:List<String>>/ /<ErrorTypeMismatch:String>/ /<DoubledValue:int>/ ; // dependency definitions D ::= <SourceNonExistingTarget> @@ -13,3 +13,6 @@ D ::= <SourceNonExistingTarget> <SourceSameAsListNode> /<TargetSameAsListNode>/ <SourceDoubledValue> /<TargetDoubledValue>/ MyList:D* ; + +// context-free endpoints +E ::= ; diff --git a/ragconnect.tests/src/test/01-input/errors/Standard.connect b/ragconnect.tests/src/test/01-input/errors/Standard.connect index bd57822a6c70198cc36698f790fa90655ed4db73..844c2e97b4f6e32e0e1f3f1392065a2f0ea3efbc 100644 --- a/ragconnect.tests/src/test/01-input/errors/Standard.connect +++ b/ragconnect.tests/src/test/01-input/errors/Standard.connect @@ -1,4 +1,4 @@ -// --- update receive definitions --- +// --- receive definitions --- // Error: there must not be two receive definitions for the same token receive B.DoubledValue ; receive B.DoubledValue using IntToInt ; @@ -18,22 +18,26 @@ receive B.ErrorTypeOfLastMapping using StringToList ; // Error: types of mappings must match (modulo inheritance) receive B.ErrorTypeMismatch using StringToList, IntToInt ; -// --- update send definitions --- +// Error: Context-Free endpoint not allowed for root nodes +receive A; + +// Error: Clash with implied endpoint definition of context-free endpoint +receive E; +receive A.E; + +// --- send definitions --- // NOT HANDLED \\ Error: the token must be resolvable within the parent type // NOT HANDLED \\ receive C.NonExisting ; -// Error: Token must be a TokenNTA (i.e., check for Token.getNTA()) -send C.ErrorNotNTA ; +// NOT HANDLED \\ // Error: from-type of first mapping must be type of Token +// NOT HANDLED \\ send C.ErrorTypeOfFirstMapping using IntToInt ; -// Error: from-type of first mapping must be type of Token -send C.ErrorTypeOfFirstMapping using IntToInt ; +// NOT HANDLED \\ // Error: to-type of last mapping must be byte[] or a supported primitive type +// NOT HANDLED \\ send C.ErrorTypeOfLastMapping1 using StringToList ; +// NOT HANDLED \\ send C.ErrorTypeOfLastMapping2 ; -// Error: to-type of last mapping must be byte[] or a supported primitive type -send C.ErrorTypeOfLastMapping1 using StringToList ; -send C.ErrorTypeOfLastMapping2 ; - -// Error: types of mappings must match (modulo inheritance) -send C.ErrorTypeMismatch using StringToList, IntToInt ; +// NOT HANDLED \\ // Error: types of mappings must match (modulo inheritance) +// NOT HANDLED \\ send C.ErrorTypeMismatch using StringToList, IntToInt ; // Error: no more than one send mapping for each TokenComponent send C.DoubledValue ; @@ -44,8 +48,8 @@ send C.DoubledValue using IntToInt ; // NOT HANDLED \\ D.SourceNonExistingTarget canDependOn D.NonExisting as NonExistingTarget ; // NOT HANDLED \\ D.NonExisting canDependOn D.TargetNonExistingSource as NonExistingSource ; -// Error: There must be a send update definition for the target token -D.SourceNoWriteDef canDependOn D.TargetNoWriteDef as NoWriteDef ; +// NOT HANDLED \\ // Error: There must be a send update definition for the target token +// NOT HANDLED \\ D.SourceNoWriteDef canDependOn D.TargetNoWriteDef as NoWriteDef ; // Error: The name of a dependency definition must not be equal to a list-node on the source D.SourceSameAsListNode canDependOn D.TargetSameAsListNode as MyList ; diff --git a/ragconnect.tests/src/test/01-input/errors/Standard.expected b/ragconnect.tests/src/test/01-input/errors/Standard.expected index 2e14612cc9fc4d7ffae435745e8b8593a22258d5..d258c2c599b54523d4ecff92eb95af8f419a9ec9 100644 --- a/ragconnect.tests/src/test/01-input/errors/Standard.expected +++ b/ragconnect.tests/src/test/01-input/errors/Standard.expected @@ -5,7 +5,9 @@ Standard.connect Line 13, column 1: No suitable default mapping found for type j Standard.connect Line 13, column 1: to-type of last mapping (java.util.List) not assignable to type of the token (String)! Standard.connect Line 16, column 1: to-type of last mapping (List) not assignable to type of the token (String)! Standard.connect Line 19, column 1: to-type of last mapping (int) not assignable to type of the token (String)! -Standard.connect Line 39, column 1: Endpoint definition already defined for C.DoubledValue -Standard.connect Line 40, column 1: Endpoint definition already defined for C.DoubledValue -Standard.connect Line 51, column 1: The name of a dependency definition must not be equal to a list-node on the source -Standard.connect Line 56, column 1: Dependency definition already defined for D with name DoubledValue +Standard.connect Line 22, column 9: Context-Free endpoint not allowed for root node A! +Standard.connect Line 26, column 1: Clash with implied, indexed endpoint definition of context-free endpoint in line 25! +Standard.connect Line 43, column 1: Endpoint definition already defined for C.DoubledValue +Standard.connect Line 44, column 1: Endpoint definition already defined for C.DoubledValue +Standard.connect Line 55, column 1: The name of a dependency definition must not be equal to a list-node on the source +Standard.connect Line 60, column 1: Dependency definition already defined for D with name DoubledValue diff --git a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/ContextFreeSimpleTest.java b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/ContextFreeSimpleTest.java new file mode 100644 index 0000000000000000000000000000000000000000..afd6c59ed3a506dd5944f02b986bb6bbc6f32c4b --- /dev/null +++ b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/ContextFreeSimpleTest.java @@ -0,0 +1,168 @@ +package org.jastadd.ragconnect.tests; + +import contextFreeSimpleInc.ast.A; +import contextFreeSimpleInc.ast.Root; +import contextFreeSimpleInc.ast.SerializationException; +import org.junit.jupiter.api.Tag; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import static org.jastadd.ragconnect.tests.TestUtils.mqttUri; +import static org.jastadd.ragconnect.tests.TestUtils.waitForMqtt; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test case "context free simple". + * + * @author rschoene - Initial contribution + */ +@Tag("Incremental") +public class ContextFreeSimpleTest extends AbstractMqttTest { + + private static final String TOPIC_UNNAMED = "unnamed"; + private static final String TOPIC_SINGLE = "single"; + private static final String TOPIC_SINGLE_ALTERNATIVE = "double"; + private static final String TOPIC_OPT = "opt"; + private static final String TOPIC_LIST_1 = "list1"; + private static final String TOPIC_LIST_2 = "list2"; + + /** Use initially created members as values in {@link #check(String, String, String, String, String)} */ + private static final String INITIAL_VALUE = null; + + private Root model; + private A unnamedA; + private A singleA; + private A optA; + private A listA1; + private A listA2; + + @Override + protected void createModel() { + model = new Root(); + unnamedA = new A().setValue("unnamed"); + singleA = new A().setValue("single"); + optA = new A().setValue("opt"); + listA1 = new A().setValue("a1"); + listA2 = new A().setValue("a2"); + + model.setA(unnamedA); + model.setSingleA(singleA); + model.setOptA(optA); + model.addListA(listA1); + model.addListA(listA2); + } + + @Override + protected void setupReceiverAndConnect(boolean writeCurrentValue) throws IOException, InterruptedException { + model.ragconnectSetupMqttWaitUntilReady(2, TimeUnit.SECONDS); + + assertTrue(unnamedA.connect(mqttUri(TOPIC_UNNAMED))); + assertTrue(singleA.connect(mqttUri(TOPIC_SINGLE))); + assertTrue(singleA.connect(mqttUri(TOPIC_SINGLE_ALTERNATIVE))); + assertTrue(optA.connect(mqttUri(TOPIC_OPT))); + assertTrue(listA1.connect(mqttUri(TOPIC_LIST_1))); + assertTrue(listA2.connect(mqttUri(TOPIC_LIST_2))); + } + + @Override + protected void communicateSendInitialValue() throws IOException, InterruptedException { + // empty + } + + @Override + protected void communicateOnlyUpdatedValue() throws IOException, InterruptedException { + check(INITIAL_VALUE, INITIAL_VALUE, INITIAL_VALUE, INITIAL_VALUE, INITIAL_VALUE); + + send(TOPIC_UNNAMED, "1"); + check("1", INITIAL_VALUE, INITIAL_VALUE, INITIAL_VALUE, INITIAL_VALUE); + + send(TOPIC_SINGLE, "2"); + check("1", "pre2", INITIAL_VALUE, INITIAL_VALUE, INITIAL_VALUE); + + send(TOPIC_SINGLE, "2.1"); + check("1", "pre2.1", INITIAL_VALUE, INITIAL_VALUE, INITIAL_VALUE); + + send(TOPIC_SINGLE, "2.2"); + check("1", "pre2.2", INITIAL_VALUE, INITIAL_VALUE, INITIAL_VALUE); + + send(TOPIC_OPT, "3"); + check("1", "pre2.2", "3post", INITIAL_VALUE, INITIAL_VALUE); + + send(TOPIC_LIST_1, "4"); + check("1", "pre2.2", "3post", "4", INITIAL_VALUE); + + send(TOPIC_LIST_2, "5"); + check("1", "pre2.2", "3post", "4", "5"); + + send(TOPIC_SINGLE_ALTERNATIVE, "fix"); + check("1", "prefix", "3post", "4", "5"); + + assertTrue(model.getSingleA().disconnect(mqttUri(TOPIC_SINGLE))); + send(TOPIC_SINGLE, "6"); + // no change to previous check since disconnected + check("1", "prefix", "3post", "4", "5"); + + send(TOPIC_SINGLE_ALTERNATIVE, "7"); + // alternative topic is still active + check("1", "pre7", "3post", "4", "5"); + + assertTrue(model.getSingleA().disconnect(mqttUri(TOPIC_SINGLE_ALTERNATIVE))); + send(TOPIC_SINGLE_ALTERNATIVE, "8"); + // no change to previous check since alternative topic is also disconnected now + check("1", "pre7", "3post", "4", "5"); + } + + private void send(String topic, String value) throws IOException, InterruptedException { + A a = new A().setValue(value); + try { + publisher.publish(topic, TestUtils.DefaultMappings.TreeToBytes(a::serialize)); + } catch (SerializationException e) { + throw new IOException(e); + } + waitForMqtt(); + } + + @SuppressWarnings("StringEquality") + private void check(String unnamedValue, String singleValue, String optValue, String list1Value, String list2Value) { + if (INITIAL_VALUE == unnamedValue) { + assertEquals(unnamedA, model.getA()); + } else { + assertNotEquals(unnamedA, model.getA()); + assertEquals(unnamedValue, model.getA().getValue()); + } + + if (INITIAL_VALUE == singleValue) { + assertEquals(singleA, model.getSingleA()); + } else { + assertNotEquals(singleA, model.getSingleA()); + assertEquals(singleValue, model.getSingleA().getValue()); + } + + if (INITIAL_VALUE == optValue) { + assertEquals(optA, model.getOptA()); + } else { + assertNotEquals(optA, model.getOptA()); + assertEquals(optValue, model.getOptA().getValue()); + } + + if (INITIAL_VALUE == list1Value) { + assertEquals(listA1, model.getListA(0)); + } else { + assertNotEquals(listA1, model.getListA(0)); + assertEquals(list1Value, model.getListA(0).getValue()); + } + + if (INITIAL_VALUE == list2Value) { + assertEquals(listA2, model.getListA(1)); + } else { + assertNotEquals(listA2, model.getListA(1)); + assertEquals(list2Value, model.getListA(1).getValue()); + } + } + + @Override + protected void closeConnections() { + + } +} diff --git a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/TestUtils.java b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/TestUtils.java index bb557f6f88783c00f3bb2115758223bae0dc6f9c..984a5b06e2bb177ae13605c58fa9a95462dd74a8 100644 --- a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/TestUtils.java +++ b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/TestUtils.java @@ -1,10 +1,14 @@ package org.jastadd.ragconnect.tests; +import com.fasterxml.jackson.core.JsonEncoding; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jastadd.ragconnect.compiler.Compiler; import org.junit.jupiter.api.Assertions; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.nio.charset.Charset; @@ -14,6 +18,7 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; import java.util.stream.Collectors; import static java.lang.Math.abs; @@ -214,6 +219,10 @@ public class TestUtils { return defaultOnlyWrite.ast.ASTNode._apply__DefaultStringToBytesMapping(input); } } + @FunctionalInterface + interface SerializeFunction<E extends Throwable> { + void accept(JsonGenerator g, String fieldName) throws E; + } public static boolean BytesToBool(byte[] input) { try { @@ -343,5 +352,13 @@ public class TestUtils { return null; } } + public static <E extends Throwable> byte[] TreeToBytes(SerializeFunction<E> serializeFunction) throws E, IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + JsonFactory factory = new JsonFactory(); + JsonGenerator generator = factory.createGenerator(outputStream, JsonEncoding.UTF8); + serializeFunction.accept(generator, null); + generator.flush(); + return outputStream.toString().getBytes(); + } } }