From 9c7c16f40f3f8a71ff0d151c2d463b8632383451 Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Mon, 7 Aug 2023 15:00:44 +0200 Subject: [PATCH 1/9] docs(modelql): some 'explanation' and 'howto' documentation for ModelQL --- .../core/pages/explanation/modelql.adoc | 23 +++++++ .../modules/core/pages/howto/modelql.adoc | 67 +++++++++++++++++++ .../modelix/modelql/typed/TypedModelQLTest.kt | 8 +-- .../modelix/modelql/client/HtmlBuilderTest.kt | 4 +- .../modelql/client/ModelQLClientTest.kt | 20 +++--- 5 files changed, 106 insertions(+), 16 deletions(-) create mode 100644 docs/global/modules/core/pages/explanation/modelql.adoc create mode 100644 docs/global/modules/core/pages/howto/modelql.adoc diff --git a/docs/global/modules/core/pages/explanation/modelql.adoc b/docs/global/modules/core/pages/explanation/modelql.adoc new file mode 100644 index 0000000000..bb5e86d8f0 --- /dev/null +++ b/docs/global/modules/core/pages/explanation/modelql.adoc @@ -0,0 +1,23 @@ += ModelQL + +When working with large models you will quickly run into performance issues +when you try to replicate the whole model into the client. + +While the data structure for model replication in Modelix supports partial loading of models +you still need a way to describe which data you need on the client. +Loading data on demand while traversing the model also results in a poor performance, +because of the potentially large number of fine-grained request. + +A first attempt to solve this problem was to disallow lazy loading +and require the client to load all required data at the beginning, +before working with the model. +A special query language was used to filter the data and an attempt to access a node that is not included by that query +results in an exception, forcing the developer to adjust the query. +While this results in a more predictable performance it's hard to maintain and still not optimal for the performance. +You have to download all the data at the beginning that you might eventually need. + +The ModelQL query language provides a more dynamic way of loading parts of the model on demand, +but still allows to reduce the number of request to a minimum. +The downside is, that it's not just a different implementation hidden behind the model-api, +but requires to use a different API. + diff --git a/docs/global/modules/core/pages/howto/modelql.adoc b/docs/global/modules/core/pages/howto/modelql.adoc new file mode 100644 index 0000000000..c39aac8abb --- /dev/null +++ b/docs/global/modules/core/pages/howto/modelql.adoc @@ -0,0 +1,67 @@ += ModelQL + +== Independent ModelQLClient + +ModelQL defines its own HTTP endpoint and provides server/client implementations for it. +The `model-server` and the `mps-model-server-plugin` already implement this endpoint. +The client can be created like this: + +[source,kotlin] +-- +val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build() +val result: List = client.query { root -> + root.children("modules").property("name").toList() +} +-- + +== Integration with LightModelClient + +When creating a `LightModelClient` you can optionally provide a `ModelQLClient` instance, +which allows to invoke `.query { ... }` (see below) on a node returned by the `LightModelClient`. + +[source,kotlin] +-- +val modelqlClient = ModelQLClient.builder().build() +val client = LightModelClient.builder().modelQLClient(modelqlClient).build() +val result: List = client.getRootNode()!!.query { + it.children("modules").property("name").toList() +} +-- + +== Type safe ModelQL API + +You can use the `model-api-gen-gradle` plugin to generate type safe extensions from your meta-model. + +[source,kotlin] +-- +val result: List = client.query { root -> + root.children("classes").ofConcept(C_ClassConcept) + .member + .ofConcept(C_StaticMethodDeclaration) + .filter { it.visibility.instanceOf(C_PublicVisibility) } + .toList() +} +-- + +== Run query on an INode + +If a query returns a node you can execute a new query starting from that node. + +[source,kotlin] +-- +val cls: ClassConcept = client.query { + it.children("classes").ofConcept(C_ClassConcept).first() +} +val names = cls.query { it.member.ofConcept(C_StaticMethodDeclaration).name.toList() } +-- + +Or you can just use the INode API to access further data of that node. +This is not recommended though, because each access sends a new query to the server. + +[source,kotlin] +-- +val cls: ClassConcept = client.query { + it.children("classes").ofConcept(C_ClassConcept).first() +} +val className = cls.name +-- diff --git a/model-api-gen-gradle-test/kotlin-generation/src/test/kotlin/org/modelix/modelql/typed/TypedModelQLTest.kt b/model-api-gen-gradle-test/kotlin-generation/src/test/kotlin/org/modelix/modelql/typed/TypedModelQLTest.kt index d26531b1fc..078434f9f0 100644 --- a/model-api-gen-gradle-test/kotlin-generation/src/test/kotlin/org/modelix/modelql/typed/TypedModelQLTest.kt +++ b/model-api-gen-gradle-test/kotlin-generation/src/test/kotlin/org/modelix/modelql/typed/TypedModelQLTest.kt @@ -106,7 +106,7 @@ class TypedModelQLTest { @Test fun simpleTest() = runTest { httpClient -> - val client = ModelQLClient("http://localhost/query", httpClient) + val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build() val result: Int = client.query { root -> root.children("classes").ofConcept(C_ClassConcept) .member @@ -118,7 +118,7 @@ class TypedModelQLTest { @Test fun test() = runTest { httpClient -> - val client = ModelQLClient("http://localhost/query", httpClient) + val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build() val result: List> = client.query { root -> root.children("classes").ofConcept(C_ClassConcept) .member @@ -132,7 +132,7 @@ class TypedModelQLTest { @Test fun testReferences() = runTest { httpClient -> - val client = ModelQLClient("http://localhost/query", httpClient) + val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build() val usedVariables: Set = client.query { root -> root.children("classes").ofConcept(C_ClassConcept) .member @@ -148,7 +148,7 @@ class TypedModelQLTest { @Test fun testReferencesFqName() = runTest { httpClient -> - val client = ModelQLClient("http://localhost/query", httpClient) + val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build() val usedVariables: Set = client.query { root -> root.children("classes").ofConcept(C_ClassConcept) .member diff --git a/modelql-client/src/jvmTest/kotlin/org/modelix/modelql/client/HtmlBuilderTest.kt b/modelql-client/src/jvmTest/kotlin/org/modelix/modelql/client/HtmlBuilderTest.kt index 4a882dd3c8..adcd0427c5 100644 --- a/modelql-client/src/jvmTest/kotlin/org/modelix/modelql/client/HtmlBuilderTest.kt +++ b/modelql-client/src/jvmTest/kotlin/org/modelix/modelql/client/HtmlBuilderTest.kt @@ -75,7 +75,7 @@ class HtmlBuilderTest { @Test fun modular() = runTest { httpClient -> - val client = ModelQLClient("http://localhost/query", httpClient) + val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build() val modelTemplate = buildModelQLFragment { val name = input.property("name").getLater() @@ -121,7 +121,7 @@ class HtmlBuilderTest { @Test fun recursive() = runTest { httpClient -> - val client = ModelQLClient("http://localhost/query", httpClient) + val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build() val modelTemplate = buildModelQLFragment { val name = input.property("name").getLater() diff --git a/modelql-client/src/jvmTest/kotlin/org/modelix/modelql/client/ModelQLClientTest.kt b/modelql-client/src/jvmTest/kotlin/org/modelix/modelql/client/ModelQLClientTest.kt index 9658bd9dc1..3f2b91cf96 100644 --- a/modelql-client/src/jvmTest/kotlin/org/modelix/modelql/client/ModelQLClientTest.kt +++ b/modelql-client/src/jvmTest/kotlin/org/modelix/modelql/client/ModelQLClientTest.kt @@ -79,7 +79,7 @@ class ModelQLClientTest { @Test fun test_count() = runTest { httpClient -> - val client = ModelQLClient("http://localhost/query", httpClient) + val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build() val result: Int = client.query { root -> root.allChildren().count() } @@ -88,7 +88,7 @@ class ModelQLClientTest { @Test fun test_properties() = runTest { httpClient -> - val client = ModelQLClient("http://localhost/query", httpClient) + val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build() val result: List = client.query { root -> root.children("modules").property("name").toList() } @@ -97,7 +97,7 @@ class ModelQLClientTest { @Test fun test_zip() = runTest { httpClient -> - val client = ModelQLClient("http://localhost/query", httpClient) + val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build() val result = client.query { root -> root.children("modules").map { it.property("name").zip(it.allChildren().nodeReference().toList()) @@ -107,7 +107,7 @@ class ModelQLClientTest { @Test fun test_zipN() = runTest { httpClient -> - val client = ModelQLClient("http://localhost/query", httpClient) + val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build() val result = client.query { root -> root.children("modules").map { it.property("name").zip( @@ -123,7 +123,7 @@ class ModelQLClientTest { @Test fun writeProperty() = runTest { httpClient -> - val client = ModelQLClient("http://localhost/query", httpClient) + val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build() val updatesNodes = client.query { root -> root.children("modules") .children("models").filter { it.property("name").contains("model1a") } @@ -148,7 +148,7 @@ class ModelQLClientTest { @Test fun writeReference() = runTest { httpClient -> - val client = ModelQLClient("http://localhost/query", httpClient) + val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build() val updatedNodes = client.query { root -> root.children("modules") .children("models").filter { it.property("name").contains("model1a") } @@ -167,7 +167,7 @@ class ModelQLClientTest { @Test fun addNewChild() = runTest { httpClient -> - val client = ModelQLClient("http://localhost/query", httpClient) + val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build() val createdNodes = client.query { root -> root.children("modules") .children("models") @@ -188,7 +188,7 @@ class ModelQLClientTest { @Test fun removeNode() = runTest { httpClient -> - val client = ModelQLClient("http://localhost/query", httpClient) + val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build() suspend fun countModels(): Int { return client.query { root -> @@ -214,7 +214,7 @@ class ModelQLClientTest { @Test fun recursiveQuery() = runTest { httpClient -> - val client = ModelQLClient("http://localhost/query", httpClient) + val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build() val descendantsNames: IFluxUnboundQuery = buildFluxQuery { it.property("name") + it.allChildren().mapRecursive() @@ -229,7 +229,7 @@ class ModelQLClientTest { @Test fun testCaching() = runTest { httpClient -> - val client = ModelQLClient("http://localhost/query", httpClient) + val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build() val result: List = client.query { root -> val numberOfNodes = root.descendants() From 0fc44f9f053521d39c684b29800ddd88ab97f4e2 Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Mon, 7 Aug 2023 15:30:00 +0200 Subject: [PATCH 2/9] docs(modelql): explain the relation to reactive streams --- .../global/modules/core/pages/explanation/modelql.adoc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/global/modules/core/pages/explanation/modelql.adoc b/docs/global/modules/core/pages/explanation/modelql.adoc index bb5e86d8f0..9627a3bd2e 100644 --- a/docs/global/modules/core/pages/explanation/modelql.adoc +++ b/docs/global/modules/core/pages/explanation/modelql.adoc @@ -21,3 +21,13 @@ but still allows to reduce the number of request to a minimum. The downside is, that it's not just a different implementation hidden behind the model-api, but requires to use a different API. +== Reactive Streams + +The query language is inspired by https://www.reactive-streams.org/[Reactive Streams] +and the execution engine uses https://kotlinlang.org/docs/flow.html[Kotlin Flows], +which is a https://kotlinlang.org/docs/coroutines-guide.html[Coroutines] compatible implementation of Reactive Streams. + +Often it's useful to know if a stream is expected to return only one element or multiple elements. +https://projectreactor.io/[Project Reactor], another implementation of Reactive Streams, +introduced the notion of `Mono` and `Flux` to distinguish them. +You will also find them in ModelQL. From 03a02bc8c7ed981437f3a1d7f3628b2ef1b83c80 Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Mon, 7 Aug 2023 16:47:30 +0200 Subject: [PATCH 3/9] docs(modelql): documentation of .zip, .mapLocal and .mapLocal2 --- .../modules/core/pages/howto/modelql.adoc | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/docs/global/modules/core/pages/howto/modelql.adoc b/docs/global/modules/core/pages/howto/modelql.adoc index c39aac8abb..968e9cf047 100644 --- a/docs/global/modules/core/pages/howto/modelql.adoc +++ b/docs/global/modules/core/pages/howto/modelql.adoc @@ -65,3 +65,81 @@ val cls: ClassConcept = client.query { } val className = cls.name -- + +== Complex query results + +While returning a list of elements is simple +the purpose of the query language is to reduce the number of request to a minimum. +This requires to combine multiple values into more complex data structures. +The `zip` operation provides a simple way of doing that: + +[source,kotlin] +-- +val result: List>> = query { db -> + db.products.map { + val id = it.id + val title = it.title + val images = it.images.toList() + id.zip(title, images) + }.toList() +} +result.forEach { println("ID: ${it.first}, Title: ${it.second}, Images: ${it.third}") } +-- + +This is suitable for combining a small number of values, +but because of the missing variable names it can be hard to read for a larger number of values +or even multiple zip operations assembled into a hierarchical data structure. + +This can be solved by defining custom data classes and using the `mapLocal` operation: + +[source,kotlin] +-- +data class MyProduct(val id: Int, val title: String, val images: List) +data class MyImage(val url: String) + +val result: List = remoteProductDatabaseQuery { db -> + db.products.map { + val id = it.id + val title = it.title + val images = it.images.mapLocal { MyImage(it) }.toList() + id.zip(title, images).mapLocal { + MyProduct(it.first, it.second, it.third) + } + }.toList() +} +result.forEach { println("ID: ${it.id}, Title: ${it.title}, Images: ${it.images}") } +-- + +The `mapLocal` operation is not just useful in combination with the `zip` operation, +but in general to create instances of classes only known to the client. + +The body of `mapLocal` is executed on the client after receiving the result from the server. +That's why you only have access to the output of the `zip` operation +and still have to use `first`, `second` and `third` inside the query. + +To make this even more readable there is a `mapLocal2` operation, +which provides a different syntax for the `zip`-`mapLocal` chain. + +[source,kotlin] +-- +data class MyProduct(val id: Int, val title: String, val images: List) +data class MyImage(val url: String) + +val result: List = query { db -> + db.products.mapLocal2 { + val id = it.id.request() + val title = it.title.request() + val images = it.images.mapLocal { MyImage(it) }.toList().request() + onSuccess { + MyNonSerializableClass(id.get(), title.get(), images.get()) + } + }.toList() +} +result.forEach { println("ID: ${it.id}, Title: ${it.title}, Images: ${it.images}") } +-- + +At the beginning of the `mapLocal2` body you invoke `request()` on all the values you need to assemble your object. +This basically ads the operand to the internal `zip` operation and returns an object that gives you access to the value +after receiving it from the server. +Inside the `onSuccess` block you assemble the local object using the previously requested values. + From e59b1cf6105c29dc185a5341f946589977645c45 Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Mon, 7 Aug 2023 17:07:39 +0200 Subject: [PATCH 4/9] docs(modelql): howto generate HTML --- .../modules/core/pages/howto/modelql.adoc | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/docs/global/modules/core/pages/howto/modelql.adoc b/docs/global/modules/core/pages/howto/modelql.adoc index 968e9cf047..a3e4d7bfc2 100644 --- a/docs/global/modules/core/pages/howto/modelql.adoc +++ b/docs/global/modules/core/pages/howto/modelql.adoc @@ -143,3 +143,47 @@ This basically ads the operand to the internal `zip` operation and returns an ob after receiving it from the server. Inside the `onSuccess` block you assemble the local object using the previously requested values. +== Kotlin HTML integration + +One use case of the query language is to build database applications +that generate HTML pages from the data stored in the model server. +You can use the https://kotlinlang.org/docs/typesafe-html-dsl.html[Kotlin HTML DSL] together with ModelQL to do that. + +Use `buildHtmlQuery` to request data from the server and render it into an HTML string: + +[source,kotlin] +-- +val html = query { + it.map(buildHtmlQuery { + val modules = input.children("modules").requestFragment<_, FlowContent> { + val moduleName = input.property("name").request() + val models = input.children("models").requestFragment<_, FlowContent> { + val modelName = input.property("name").request() + onSuccess { + div { + h2 { + +"Model: ${modelName.get()}" + } + } + } + } + onSuccess { + div { + h1 { + +"Module: ${moduleName.get()}" + } + insertFragment(models) + } + } + } + onSuccess { + body { + insertFragment(modules) + } + } + }) +} +-- + +`buildHtmlQuery` and the `requestFragment` operation are similar to the `mapLocal2` operation, +but inside the `onSuccess` block you use the Kotlin HTML DSL. From 50f5eec366dd06e6fce28fb1405e3c370456a2de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20Li=C3=9Fon?= Date: Fri, 25 Aug 2023 15:02:00 +0200 Subject: [PATCH 5/9] docs(modelql): fixed some of the review comments --- .../core/pages/explanation/modelql.adoc | 8 ++++---- .../modules/core/pages/howto/modelql.adoc | 18 +++++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/global/modules/core/pages/explanation/modelql.adoc b/docs/global/modules/core/pages/explanation/modelql.adoc index 9627a3bd2e..0fb57f3773 100644 --- a/docs/global/modules/core/pages/explanation/modelql.adoc +++ b/docs/global/modules/core/pages/explanation/modelql.adoc @@ -3,7 +3,7 @@ When working with large models you will quickly run into performance issues when you try to replicate the whole model into the client. -While the data structure for model replication in Modelix supports partial loading of models +While the data structure for model replication in Modelix supports partial loading of models, you still need a way to describe which data you need on the client. Loading data on demand while traversing the model also results in a poor performance, because of the potentially large number of fine-grained request. @@ -12,9 +12,9 @@ A first attempt to solve this problem was to disallow lazy loading and require the client to load all required data at the beginning, before working with the model. A special query language was used to filter the data and an attempt to access a node that is not included by that query -results in an exception, forcing the developer to adjust the query. -While this results in a more predictable performance it's hard to maintain and still not optimal for the performance. -You have to download all the data at the beginning that you might eventually need. +resulted in an exception, forcing the developer to adjust the query. +While this results in a more predictable performance, it is also hard to maintain and still not optimal for the performance. +You have to download all the data at the beginning that you might eventually need, potentially exceeding the available memory of the system. The ModelQL query language provides a more dynamic way of loading parts of the model on demand, but still allows to reduce the number of request to a minimum. diff --git a/docs/global/modules/core/pages/howto/modelql.adoc b/docs/global/modules/core/pages/howto/modelql.adoc index a3e4d7bfc2..7c743c6a12 100644 --- a/docs/global/modules/core/pages/howto/modelql.adoc +++ b/docs/global/modules/core/pages/howto/modelql.adoc @@ -31,6 +31,7 @@ val result: List = client.getRootNode()!!.query { == Type safe ModelQL API You can use the `model-api-gen-gradle` plugin to generate type safe extensions from your meta-model. +Specify the `modelqlKotlinDir` property to enable the generation. [source,kotlin] -- @@ -45,7 +46,7 @@ val result: List = client.query { root -> == Run query on an INode -If a query returns a node you can execute a new query starting from that node. +If a query returns a node, you can execute a new query starting from that node. [source,kotlin] -- @@ -55,8 +56,8 @@ val cls: ClassConcept = client.query { val names = cls.query { it.member.ofConcept(C_StaticMethodDeclaration).name.toList() } -- -Or you can just use the INode API to access further data of that node. -This is not recommended though, because each access sends a new query to the server. +For convenience, it's possible to access further data of that node using the INode API, +but this is not recommended though, because each access sends a new query to the server. [source,kotlin] -- @@ -68,9 +69,9 @@ val className = cls.name == Complex query results -While returning a list of elements is simple +While returning a list of elements is simple, the purpose of the query language is to reduce the number of request to a minimum. -This requires to combine multiple values into more complex data structures. +This requires combining multiple values into more complex data structures. The `zip` operation provides a simple way of doing that: [source,kotlin] @@ -97,7 +98,7 @@ This can be solved by defining custom data classes and using the `mapLocal` oper data class MyProduct(val id: Int, val title: String, val images: List) data class MyImage(val url: String) -val result: List = remoteProductDatabaseQuery { db -> +val result: List = remoteProductDatabaseQuery { db -> db.products.map { val id = it.id val title = it.title @@ -125,13 +126,12 @@ which provides a different syntax for the `zip`-`mapLocal` chain. data class MyProduct(val id: Int, val title: String, val images: List) data class MyImage(val url: String) -val result: List = query { db -> - db.products.mapLocal2 { +val result: List = query { db -> val id = it.id.request() val title = it.title.request() val images = it.images.mapLocal { MyImage(it) }.toList().request() onSuccess { - MyNonSerializableClass(id.get(), title.get(), images.get()) + MyProduct(id.get(), title.get(), images.get()) } }.toList() } From 977443006a1cf750f99b3334f7311cd577aab606 Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Tue, 24 Oct 2023 13:23:37 +0200 Subject: [PATCH 6/9] fix(modelql): renamed mapLocal2 to buildLocalMapping --- docs/global/modules/core/pages/howto/modelql.adoc | 7 ++++--- .../org/modelix/modelql/core/LocalMappingStep.kt | 14 ++++++++++++-- .../kotlin/org/modelix/modelql/core/ModelQLTest.kt | 8 ++++---- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/docs/global/modules/core/pages/howto/modelql.adoc b/docs/global/modules/core/pages/howto/modelql.adoc index 7c743c6a12..e3aedce664 100644 --- a/docs/global/modules/core/pages/howto/modelql.adoc +++ b/docs/global/modules/core/pages/howto/modelql.adoc @@ -118,7 +118,7 @@ The body of `mapLocal` is executed on the client after receiving the result from That's why you only have access to the output of the `zip` operation and still have to use `first`, `second` and `third` inside the query. -To make this even more readable there is a `mapLocal2` operation, +To make this even more readable there is a `buildLocalMapping` operation, which provides a different syntax for the `zip`-`mapLocal` chain. [source,kotlin] @@ -127,6 +127,7 @@ data class MyProduct(val id: Int, val title: String, val images: List) data class MyImage(val url: String) val result: List = query { db -> + db.products.buildLocalMapping { val id = it.id.request() val title = it.title.request() val images = it.images.mapLocal { MyImage(it) }.toList().request() @@ -138,7 +139,7 @@ val result: List = query { db -> result.forEach { println("ID: ${it.id}, Title: ${it.title}, Images: ${it.images}") } -- -At the beginning of the `mapLocal2` body you invoke `request()` on all the values you need to assemble your object. +At the beginning of the `buildLocalMapping` body you invoke `request()` on all the values you need to assemble your object. This basically ads the operand to the internal `zip` operation and returns an object that gives you access to the value after receiving it from the server. Inside the `onSuccess` block you assemble the local object using the previously requested values. @@ -185,5 +186,5 @@ val html = query { } -- -`buildHtmlQuery` and the `requestFragment` operation are similar to the `mapLocal2` operation, +`buildHtmlQuery` and the `requestFragment` operation are similar to the `buildLocalMapping` operation, but inside the `onSuccess` block you use the Kotlin HTML DSL. diff --git a/modelql-core/src/commonMain/kotlin/org/modelix/modelql/core/LocalMappingStep.kt b/modelql-core/src/commonMain/kotlin/org/modelix/modelql/core/LocalMappingStep.kt index 8c1bb39a36..99a722f151 100644 --- a/modelql-core/src/commonMain/kotlin/org/modelix/modelql/core/LocalMappingStep.kt +++ b/modelql-core/src/commonMain/kotlin/org/modelix/modelql/core/LocalMappingStep.kt @@ -107,14 +107,24 @@ class ExecuteLocalStep(transformation: (In) -> Out) : LocalMappingStep< } } +@Deprecated("renamed to buildLocalMapping", ReplaceWith("buildLocalMapping(body)")) fun IFluxStep.mapLocal2(body: ILocalMappingBuilder.(IMonoStep) -> Unit): IFluxStep { - return map { it.mapLocal2(body) } + return buildLocalMapping(body) } +fun IFluxStep.buildLocalMapping(body: ILocalMappingBuilder.(IMonoStep) -> Unit): IFluxStep { + return map { it.buildLocalMapping(body) } +} + +@Deprecated("renamed to buildLocalMapping", ReplaceWith("buildLocalMapping(body)")) fun IMonoStep.mapLocal2(body: ILocalMappingBuilder.(IMonoStep) -> Unit): IMonoStep { + return buildLocalMapping(body) +} + +fun IMonoStep.buildLocalMapping(body: ILocalMappingBuilder.(IMonoStep) -> Unit): IMonoStep { val builder = LocalMappingBuilder() builder.apply { - body(this@mapLocal2) + body(this@buildLocalMapping) } return builder.compileProcessingOutputStep() } diff --git a/modelql-core/src/commonTest/kotlin/org/modelix/modelql/core/ModelQLTest.kt b/modelql-core/src/commonTest/kotlin/org/modelix/modelql/core/ModelQLTest.kt index ad916105b5..6501b48c6a 100644 --- a/modelql-core/src/commonTest/kotlin/org/modelix/modelql/core/ModelQLTest.kt +++ b/modelql-core/src/commonTest/kotlin/org/modelix/modelql/core/ModelQLTest.kt @@ -250,7 +250,7 @@ class ModelQLTest { @Test fun testMapLocal2_unusedInput() = runTestWithTimeout { val result = remoteProductDatabaseQuery { db -> - db.products.mapLocal2 { + db.products.buildLocalMapping { val title = "xxx".asMono().getLater() onSuccess { "Title: " + title.get() @@ -262,9 +262,9 @@ class ModelQLTest { } @Test - fun testMapLocal2() = runTestWithTimeout { + fun testLocalMappingBuilder() = runTestWithTimeout { val result = remoteProductDatabaseQuery { db -> - db.products.mapLocal2 { + db.products.buildLocalMapping { val title = it.title.getLater() onSuccess { "Title: " + title.get() @@ -278,7 +278,7 @@ class ModelQLTest { @Test fun testZipOrder() = runTestWithTimeout { val result = remoteProductDatabaseQuery { db -> - db.products.flatMap { it.zip(it.images.assertNotEmpty()) }.mapLocal2 { + db.products.flatMap { it.zip(it.images.assertNotEmpty()) }.buildLocalMapping { val product = it.first.getLater() val image = it.second.getLater() onSuccess { From 69bcc07d3663efb56cf2dd6a9a631e747bab1271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20Li=C3=9Fon?= Date: Wed, 25 Oct 2023 09:54:16 +0200 Subject: [PATCH 7/9] docs(modelql): apply suggestions from code review Co-authored-by: Johannes Wienke --- docs/global/modules/core/pages/explanation/modelql.adoc | 4 ++-- docs/global/modules/core/pages/howto/modelql.adoc | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/global/modules/core/pages/explanation/modelql.adoc b/docs/global/modules/core/pages/explanation/modelql.adoc index 0fb57f3773..c198f19e84 100644 --- a/docs/global/modules/core/pages/explanation/modelql.adoc +++ b/docs/global/modules/core/pages/explanation/modelql.adoc @@ -17,8 +17,8 @@ While this results in a more predictable performance, it is also hard to maintai You have to download all the data at the beginning that you might eventually need, potentially exceeding the available memory of the system. The ModelQL query language provides a more dynamic way of loading parts of the model on demand, -but still allows to reduce the number of request to a minimum. -The downside is, that it's not just a different implementation hidden behind the model-api, +but still allows reducing the number of request to a minimum. +The downside is that it's not just a different implementation hidden behind the model-api, but requires to use a different API. == Reactive Streams diff --git a/docs/global/modules/core/pages/howto/modelql.adoc b/docs/global/modules/core/pages/howto/modelql.adoc index e3aedce664..870c2a7e90 100644 --- a/docs/global/modules/core/pages/howto/modelql.adoc +++ b/docs/global/modules/core/pages/howto/modelql.adoc @@ -17,7 +17,7 @@ val result: List = client.query { root -> == Integration with LightModelClient When creating a `LightModelClient` you can optionally provide a `ModelQLClient` instance, -which allows to invoke `.query { ... }` (see below) on a node returned by the `LightModelClient`. +which allows invoking `.query { ... }` (see below) on a node returned by the `LightModelClient`. [source,kotlin] -- @@ -28,7 +28,7 @@ val result: List = client.getRootNode()!!.query { } -- -== Type safe ModelQL API +== Type-safe ModelQL API You can use the `model-api-gen-gradle` plugin to generate type safe extensions from your meta-model. Specify the `modelqlKotlinDir` property to enable the generation. @@ -139,8 +139,8 @@ val result: List = query { db -> result.forEach { println("ID: ${it.id}, Title: ${it.title}, Images: ${it.images}") } -- -At the beginning of the `buildLocalMapping` body you invoke `request()` on all the values you need to assemble your object. -This basically ads the operand to the internal `zip` operation and returns an object that gives you access to the value +At the beginning of the `buildLocalMapping` body, you invoke `request()` on all the values you need to assemble your object. +This basically adds the operand to the internal `zip` operation and returns an object that gives you access to the value after receiving it from the server. Inside the `onSuccess` block you assemble the local object using the previously requested values. From 8a79678d47f0a2c5565b4fd85b87d53c0e1332ef Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Wed, 25 Oct 2023 10:09:22 +0200 Subject: [PATCH 8/9] docs(modelql): added link to modelqlKotlinDir --- docs/global/modules/core/pages/howto/modelql.adoc | 2 +- .../core/pages/reference/component-model-api-gen-gradle.adoc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/global/modules/core/pages/howto/modelql.adoc b/docs/global/modules/core/pages/howto/modelql.adoc index 870c2a7e90..b7869bcc8b 100644 --- a/docs/global/modules/core/pages/howto/modelql.adoc +++ b/docs/global/modules/core/pages/howto/modelql.adoc @@ -31,7 +31,7 @@ val result: List = client.getRootNode()!!.query { == Type-safe ModelQL API You can use the `model-api-gen-gradle` plugin to generate type safe extensions from your meta-model. -Specify the `modelqlKotlinDir` property to enable the generation. +Specify the link:../reference/component-model-api-gen-gradle.adoc#model-api-gen-gradle_attributes_modelqlKotlinDir[modelqlKotlinDir] property to enable the generation. [source,kotlin] -- diff --git a/docs/global/modules/core/pages/reference/component-model-api-gen-gradle.adoc b/docs/global/modules/core/pages/reference/component-model-api-gen-gradle.adoc index 8240425e53..7dbeefe1f3 100644 --- a/docs/global/modules/core/pages/reference/component-model-api-gen-gradle.adoc +++ b/docs/global/modules/core/pages/reference/component-model-api-gen-gradle.adoc @@ -82,7 +82,7 @@ Inside of the `metamodel` block the following settings can be configured. |File |Target Kotlin directory of the generator -|`modelqlKotlinDir` +|`modelqlKotlinDir` [[model-api-gen-gradle_attributes_modelqlKotlinDir,modelqlKotlinDir]] |File |The generation of the ModelQL API is optional, because the output has a dependency on the ModelQL runtime. If this option is set, you have to add a dependency on `org.modelix:modelql-typed`. From 405a00b9485b6c7a224580d4e6cd5601f6aa3c4a Mon Sep 17 00:00:00 2001 From: Sascha Lisson Date: Wed, 25 Oct 2023 10:11:20 +0200 Subject: [PATCH 9/9] docs(modelql): added link to INode --- docs/global/modules/core/pages/howto/modelql.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/global/modules/core/pages/howto/modelql.adoc b/docs/global/modules/core/pages/howto/modelql.adoc index b7869bcc8b..7257db0f4c 100644 --- a/docs/global/modules/core/pages/howto/modelql.adoc +++ b/docs/global/modules/core/pages/howto/modelql.adoc @@ -56,7 +56,7 @@ val cls: ClassConcept = client.query { val names = cls.query { it.member.ofConcept(C_StaticMethodDeclaration).name.toList() } -- -For convenience, it's possible to access further data of that node using the INode API, +For convenience, it's possible to access further data of that node using the https://api.modelix.org/3.6.0/model-api/org.modelix.model.api/-i-node/index.html?query=interface%20INode[INode] API, but this is not recommended though, because each access sends a new query to the server. [source,kotlin]