Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

initial formatter schema #12

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open

initial formatter schema #12

wants to merge 12 commits into from

Conversation

maytheu
Copy link
Contributor

@maytheu maytheu commented Sep 11, 2023

Adding and updating uddl formatter

@steve-hickman-epistimis
Copy link
Contributor

It looks like you headed in the right direction

@maytheu
Copy link
Contributor Author

maytheu commented Sep 12, 2023

I want to test the formatConceptualEntity() but I don't know where I'll call it

@steve-hickman-epistimis
Copy link
Contributor

steve-hickman-epistimis commented Sep 12, 2023

I want to test the formatConceptualEntity() but I don't know where I'll call it

The protected format(node: AstNode): void method is the only entrypoint for this class. Everything you create must be called directly or indirectly from there. Look at the example here.

Here's what to do: Modify the protected format(node: AstNode): void method to call the format methods for anything that can appear at the top level. Then, inside of those individual format methods, call the format methods for each of the elements that they can hold. And so on.

This is effectively what happens in UddlFormatter.xtend. The top level format method call there is to def dispatch void format(DataModel dataModel, extension IFormattableDocument document). You can see how it calls the format methods of the things it can contain - and they in turn call the format methods of what they can contain. And so on.

What does this mean for testing? It means you never test just one format method by changing what you call - because you have no control of how the format methods get called. You test format methods by what you pass in to be formatted. So to test the formatConceptualEntity() method you need to: A) make sure it is called appropriately as described above; 2) Pass in a string that contains one or more ConceptualEntities (along with whatever else is necessary to create a valid UDDL document).

One thing to be aware of: in XTend, a function can be called either using the first parameter as a parameter or as the receiver object on which the function is invoked.. What this means is that in the formatEntity function on lines 202-208, line 205 is a call to the format method for ConceptualComposition on lines 229-231. How do I know this? Because the c on line 205 is a ConceptualComposition. How do I know that? Because I've read the UDDL specification and know that the composition collection in a ConceptualEntity contains instances of ConceptualComposition. This kind of thing occurs throughout the UddlFormater.xtend code. Keep it in mind if you're trying to figure out what the code is doing.

@maytheu
Copy link
Contributor Author

maytheu commented Sep 14, 2023

Noted

@maytheu
Copy link
Contributor Author

maytheu commented Sep 14, 2023

So far i have updated the format() as suggested and check if isConceptualAssociation(), isConceptualEntity() and isConceptualBasisEntityisConceptualBasisEntity() and callinng its respective method.

I have tried to format ConceptualBasisEntity in formatConceptualBasisEntity(), I don't know if that the way to go for formatting ConceptualBasisEntity.

@steve-hickman-epistimis
Copy link
Contributor

steve-hickman-epistimis commented Sep 14, 2023

Reviewing the Langium formatting doc, and the examples ( compare domain-model.langium with domain-model-formatter.ts ), you can call every formatting routine directly in format(AstNode) as though that type of AstNode were at the top level because "The AbstractFormatter calls this method for every node of our model". If you do this then you must always treat each node as though all surrounding context has been set separately - and then ensure that you write the code that way. What does that mean? It means that any time you want to format something with respect to the surrounding context, you must treat it as though the surrounding context has already been formatted and you can only control the position/ formatting of the current node relative to that.

Another alternative is to manage the hierarchy yourself. This is the approach I attempted to describe previously (My apologies if I didn't do a good job above.) For that approach, you need to review the UDDL Spec (publicly available here - see also here ), Chapter 5 or look at the universal-data-definition-language.langium file to understand what is at the top level and what is contained in something else. If you look at the .langium file, you see the entry rule on line 6. This is the top level for every file. The rules referred to in the entry rule indicate what is 'contained' in that. And so on. This tells you where you should call each format method.

Don't do both - pick one approach and stick with it. Attempting to do both will result in some nodes being formatted more than once, which will be slower, and could result in conflicts that are hard to debug.

The approach of calling everything directly from the format(AstNode) method is probably the best approach because that method will be called for every node anyway.

@maytheu
Copy link
Contributor Author

maytheu commented Sep 14, 2023

Thanks for the headsup and clarification

@maytheu
Copy link
Contributor Author

maytheu commented Sep 18, 2023

I faced an issue previously where the tests failed when I didn't have a formatter for the UDDL definitions. However, after adding UDDL definitions to the formatter file, the test file passed successfully. Surprisingly, when I commented out the changes I made in the formatter file, the tests still passed. This behavior is unexpected, and I'm looking for guidance to understand what might be causing it and where I might be overlooking something.

Copy link
Contributor

@steve-hickman-epistimis steve-hickman-epistimis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comments for an explanation of why the comments had no effect on the tests passing.

if(ast.isConceptualAssociation(node)){
this.formatConceptualAssociation(node)
}
// test passes with this commetned check
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test you have defined does not include a ConceptualAssociation, so commenting this method out will have no effect on test success/ failure.

}

// test passes with this commetned check
// if(ast.isConceptualBasisEntity(node)){
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test you have defined only has 1 ConceptualBasisEntity. The formatting you see around it is caused by 2 things: 1) the surrounding formatting is done by formatObj for the containing CDM; 2) A ConceptualBasisEntity is also a ConceptualElement. So when you comment out this method, the formatConceptualElement will still be called.

if you want to make sure you know the effect of a formatting method, you should have multiple instances of that type in your test. In this case, it means including multiple ConceptualBasisEntity instances. At a minimum, have 3. It may also make sense to have instances of different types intermingled where possible just to check on the effect of formatting one type on adjacent instances of a different type.

This brings up an important point: You can't just have if checks in the top level format(AstNode) method. If you do that, then every if is checked for each node and every if check that returns true will result in a format method being called - in the order they show up in format(AstNode). If multiple format methods are called for the same AstNode,not only will that be slower than necessary but also only the last one called will matter, because it will overwrite the effects of all format methods previously called for that AstNode.

Instead, you need to make sure to use if ... else if so that only a single formatting method is called for each AstNode. And you need to make sure to put the formatting methods in order from most specific to most general based on the hierarchy described in the .langium file, because you want the first test to pass to be the most specific formatter applicable.

cdm Conceptual "Need to start at Conceptual Level"
{

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you really want all these blank lines after formatting? You've got a blank line after every '{' and '}'. That's going to make some files a lot longer - which will force users to scroll. When people have to scroll, they forget what they can't see - which makes it harder to understand complex content.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test would not pass without the blank lines in the after block

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a test does not pass, you have a choice: change the test (what you did) or change the code being tested. The point I'm making above is that the formatter currently inserts those blank lines - which is why your test needs the blank lines to pass. My question is: do you want the formatter to do that? The goal of formatting is to put the text into a format that is easy to read and understand. Every decision you make about how the formatter works should be made with that in mind.


it('should format cdm with conceptual basis',async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While you can have tests for each individual type, you may find that a test that combines multiple types is sufficient.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a previous commit, I merged conceptual entities and conceptual associations into one test case. During this process, only formatter for conceptual entities was checked. I then attempted to verify whether I would encounter similar behavior for conceptual basis and conceptual domain as well.

it("should format cdm with conceptual entity", async () => {
await universalDataDefinitionLanguageFormatting({
before:
'dm PPT "Base data structures to support People, Places and Things"{cdm Conceptual "Need to start at Conceptual Level"{centity AddressableEntity "Any entity that is addressable in some way" {};}}',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In your current tests, all the 'before' text is on a single line. You should also have the same tests where the 'before' text has each token on a separate line (you can do this with a find/replace that replaces blanks the before text with CRs).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted

Copy link
Contributor

@steve-hickman-epistimis steve-hickman-epistimis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formatContainer should only be used to format containers, not the individual elements within a container.

else if(ast.isLogicalEntity(node)){
this.formatLogicalEntity(node)
}
else if(ast.isLogicalAssociation(node)){
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All LogicalAssociations are also LogicalEntities => Swap lines 55-57 and 58-60 so you check for LogicalAssociation first

@@ -36,7 +36,43 @@ export class UniversalDataDefinitionLanguageFormatter extends AbstractFormatter
}
else if(ast.isConceptualEntity(node)){
this.formatConceptualEntity(node)
}
}
else if(ast.isLogicalEnumerated(node)){
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you choose the ordering of if-else if ... here? Make sure to document it. You don't want someone else to come along after you and make some change to the ordering that causes problems.

else if(ast.isPlatformStruct(node)){
this.formatPlatformStruct(node)
}
else if(ast.isPlatformAssociation(node)){
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All PlatformAssociations are also PlatformEntities. The check isPlatformAssociation must be before the check isPlatformEntity .

protected formatLogicalEnumerated(lenum: ast.LogicalEnumerated): void {
this.formatContainer(lenum)
lenum.label.forEach(label => {
this.formatContainer(label)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you calling formatContainer on label? It's not a container.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The label is an array of LogicalEnumeratedSet | LogicalEnumerationLabel which are both containers

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be a LogicalEnumeratedSet, which is a container, or a LogicalEnumerationLabel, which is not a container. In these circumstances, remember that the top level format method will figure out the type of label - so all you need to do is call format, which will then call the appropriate formatter for either LogicalEnumerationLabel or LogicalEnumeratedSet. Your current approach will not handle multiple levels of nesting appropriately.


protected formatLogicalMeasurementSystem(sys: ast.LogicalMeasurementSystem): void {
this.formatContainer(sys);
sys.constraint.forEach(sysConst => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

measurementSystemAxis is also a collection.

protected formatLogicalMeasurementSystemAxis(sysAxis: ast.LogicalMeasurementSystemAxis): void {
this.formatContainer(sysAxis);
sysAxis.constraint.forEach(sysConst => {
this.formatContainer(sysConst);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Individual constraints are not containers.

protected formatLogicalReferencePoint(refPoint: ast.LogicalReferencePoint): void {
this.formatContainer(refPoint);
refPoint.referencePointPart.forEach(pointPart => {
this.formatContainer(pointPart);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An individual ReferencePointPart is not a container

protected formatLogicalMeasurement(measure: ast.LogicalMeasurement): void {
this.formatContainer(measure);
measure.attribute.forEach(attr => {
this.formatContainer(attr)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Individual attributes are not containers

this.formatContainer(attr)
})
measure.constraint.forEach(mesConst => {
this.formatContainer(mesConst)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

individual constraints are not containers

protected formatLogicalEntity(entity: ast.LogicalEntity): void {
this.formatContainer(entity);
entity.composition.forEach(comp => {
this.formatContainer(comp);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Individual composition elements are not containers

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the heads up, will make the changes and revert. I will get back if I am stuck.

@steve-hickman-epistimis
Copy link
Contributor

Make sure to post your test results each time you want a review. They can be a screenshot of you running the tests and them passing. Or attach a file with the results. The point here is that I should be able to verify that tests pass without running them myself.

@maytheu
Copy link
Contributor Author

maytheu commented Oct 5, 2023

Yes, I want to write all test and make sure it passes before taking screenshot

@maytheu
Copy link
Contributor Author

maytheu commented Oct 9, 2023

Screenshot from 2023-10-09 20-20-46

Copy link
Contributor

@steve-hickman-epistimis steve-hickman-epistimis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're making steady progress.

protected formatLogicalEnumerated(lenum: ast.LogicalEnumerated): void {
this.formatContainer(lenum)
lenum.label.forEach(label => {
this.formatContainer(label)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be a LogicalEnumeratedSet, which is a container, or a LogicalEnumerationLabel, which is not a container. In these circumstances, remember that the top level format method will figure out the type of label - so all you need to do is call format, which will then call the appropriate formatter for either LogicalEnumerationLabel or LogicalEnumeratedSet. Your current approach will not handle multiple levels of nesting appropriately.

csa logicalCoordinate "Logical system axis";
coord GeographicCoordinates "Represents geographic coordinates" {

axis:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest that the prompt and its value be on the same line ( axis: "WGS84"). That will be easier to understand.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants