diff --git a/docs/StardustDocs/topics/Compiler-Plugin.md b/docs/StardustDocs/topics/Compiler-Plugin.md index ec8a59934d..95cbef1234 100644 --- a/docs/StardustDocs/topics/Compiler-Plugin.md +++ b/docs/StardustDocs/topics/Compiler-Plugin.md @@ -135,7 +135,7 @@ is displayed when you hover on an expression or variable: ### @DataSchema declarations -Untyped DataFrame can be assigned a data schema - top-level interface or class that describes names and types of columns in the dataframe. +Untyped DataFrame can be assigned a data schema - top-level interface or data class that describes names and types of columns in the dataframe. ```kotlin @DataSchema diff --git a/docs/StardustDocs/topics/Home.topic b/docs/StardustDocs/topics/Home.topic index 70c6bfb631..5d98c36005 100644 --- a/docs/StardustDocs/topics/Home.topic +++ b/docs/StardustDocs/topics/Home.topic @@ -19,16 +19,16 @@ First steps - + - Reading from files: CSV, JSON, ApacheArrow + Reading from files: CSV, JSON, Excel and Apache Arrow Featured topics - - + + Kotlin Compiler Plugin diff --git a/docs/StardustDocs/topics/extensionPropertiesApi.md b/docs/StardustDocs/topics/extensionPropertiesApi.md index 5d303751ab..ece739a575 100644 --- a/docs/StardustDocs/topics/extensionPropertiesApi.md +++ b/docs/StardustDocs/topics/extensionPropertiesApi.md @@ -2,7 +2,7 @@ When working with a [`DataFrame`](DataFrame.md), the most convenient and reliable way to access its columns — including for operations and retrieving column values -in row expressions — is through *auto-generated extension properties*. +in [row expressions](DataRow.md#row-expressions) — is through *auto-generated extension properties*. They are generated based on a [dataframe schema](schemas.md), with the name and type of properties inferred from the name and type of the corresponding columns. It also works for all types of hierarchical dataframes. @@ -24,10 +24,17 @@ It also works for all types of hierarchical dataframes. Consider a simple hierarchical dataframe from . -This table consists of two columns: `name`, which is a `String` column, and `info`, -which is a [**column group**](DataColumn.md#columngroup) containing two nested -[value columns](DataColumn.md#valuecolumn) — -`age` of type `Int`, and `height` of type `Double`. +> Note that this is not a regular CSV file — it contains a column with embedded JSON values. +> +> To read such files correctly, both the [`dataframe-csv`](Modules.md#dataframe-csv) +> and [`dataframe-json`](Modules.md#dataframe-json) modules must be included. +> {style="note"} + +This dataframe consists of two columns: +- `name`, which is a `String` column +- `info`, which is a [column group](DataColumn.md#columngroup) containing two nested [value columns](DataColumn.md#valuecolumn): + - `age` of type `Int` + - `height` of type `Double` @@ -119,24 +126,27 @@ You can do it quickly with [`generate..()` methods](DataSchemaGenerationMethods. Define schemas: ```kotlin +// Data schema of the "info" column group @DataSchema -data class PersonInfo( - val age: Int, +interface Info { + val age: Int val height: Float -) +} +// Data schema of the entire DataFrame @DataSchema -data class Person( - val info: PersonInfo, +interface Person { + val info: Info val name: String -) +} +``` ``` Read the [`DataFrame`](DataFrame.md) from the CSV file and specify the schema with [`.convertTo()`](convertTo.md) or [`cast()`](cast.md): ```kotlin -val df = DataFrame.readCsv("example.csv").convertTo() +val df = DataFrame.readCsv("example.csv").cast() ``` Extensions for this `DataFrame` will be generated automatically by the plugin, @@ -229,3 +239,55 @@ interface Info { val df = dataFrameOf("size\nin:inches" to listOf(..)).cast() df.sizeInInches ``` + +## Custom extension properties + +Sometimes it is useful to define your own extension properties +based on a [data schema](schema.md). + +For example, consider a simple dataframe with two columns and the following `BranchData` schema: + +```kotlin +@DataSchema +interface BranchData { + val expenses: Long + val revenue: Long +} +``` + +```kotlin +// Read DataFrame and cast its type parameter to BranchData +val df = DataFrame.readCsv("branchData.csv").cast() +``` + +You can define an extension property for `DataRow` +to create a convenient shortcut: + +```kotlin +// Use generated extension properties to create a new one +val DataRow.profit get() = revenue - expenses +``` + +You can then use it, for example, in [row expressions](DataRow.md#row-expressions): + +```kotlin +val dfProfitable = df.filter { it.profit > 0 } +``` + +Note that if you change the actual schema of a dataframe +(by performing operations that modify its structure), +this extension property can no longer be used, +because it is tied to the specific schema. + +```kotlin +df.add("name") { "branchName" } + // unresolved because of `add` + .filter { it.profit > 0 } +``` + +However, you can work around this by casting back to the original schema: + +```kotlin +df.add("name") { "branchName" } + .filter { it.cast().profit > 0 } +``` diff --git a/docs/StardustDocs/topics/schemas/DataSchemaGenerationMethods.md b/docs/StardustDocs/topics/schemas/DataSchemaGenerationMethods.md index ec73ddb7a8..e053c1c390 100644 --- a/docs/StardustDocs/topics/schemas/DataSchemaGenerationMethods.md +++ b/docs/StardustDocs/topics/schemas/DataSchemaGenerationMethods.md @@ -17,32 +17,23 @@ Generate useful Kotlin definitions based on your DataFrame structure. Special utility functions that generate code of useful Kotlin definitions (returned as a `String`) based on the current `DataFrame` schema. -## generateDataClasses +## generateInterfaces ```kotlin -inline fun DataFrame.generateDataClasses( - markerName: String? = null, - extensionProperties: Boolean = false, - visibility: MarkerVisibility = MarkerVisibility.IMPLICIT_PUBLIC, - useFqNames: Boolean = false, - nameNormalizer: NameNormalizer = NameNormalizer.default, - nestedMarkerNameProvider: MarkerNameProvider = MarkerNameProvider.fromColumnName, -): CodeString -``` +inline fun DataFrame.generateInterfaces(): CodeString -Generates Kotlin data classes corresponding to the `DataFrame` schema -(including all nested `DataFrame` columns and column groups). +fun DataFrame.generateInterfaces(markerName: String): CodeString +``` -Useful when you want to: +Generates [`@DataSchema`](schemas.md) interfaces for this `DataFrame` +(including all nested `DataFrame` columns and column groups) as Kotlin interfaces. -- Work with the data as regular Kotlin data classes. -- Convert a dataframe to instantiated data classes with `df.toListOf()`. -- Work with data classes serialization. -- Extract structured types for further use in your application. +This is useful when working with the [compiler plugin](Compiler-Plugin.md) +in cases where the schema cannot be inferred automatically from the source. -### Arguments {id="generateDataClasses-arguments"} +### Arguments {id="generateInterfaces-arguments"} -* `markerName`: `String?` — The base name to use for generated data classes. +* `markerName`: `String?` — The base name to use for generated interfaces. If `null`, uses the `T` type argument of `DataFrame` simple name. Default: `null`. * `extensionProperties`: `Boolean` – Whether to generate [extension properties](extensionPropertiesApi.md) @@ -59,69 +50,95 @@ Useful when you want to: Kotlin-style identifiers. Generated properties will still refer to columns by their actual name using the `@ColumnName` annotation. Default: `NameNormalizer.default`. -* `nestedMarkerNameProvider`: `MarkerNameProvider` – Strategy for generating names for nested data schema declarations (markers). - - `MarkerNameProvider.fromColumnName` (default) generates descriptive names from the column names. - - `MarkerNameProvider.PredefinedName` uses the name of the root marker for all nested declarations and appends a numerical suffix to resolve name conflicts. - Default: `MarkerNameProvider.fromColumnName`. +* `nestedMarkerNameProvider`: `MarkerNameProvider` – Strategy for generating names for nested data schema declarations (markers). + - `MarkerNameProvider.fromColumnName` (default) generates descriptive names from the column names. + - `MarkerNameProvider.PredefinedName` uses the name of the root marker for all nested declarations and appends a numerical suffix to resolve name conflicts. + Default: `MarkerNameProvider.fromColumnName`. -### Returns {id="generateDataClasses-returns"} +### Returns {id="generateInterfaces-returns"} -* `CodeString` — A value class wrapper for `String`, containing - the generated Kotlin code of `data class` declarations and optionally [extension properties](extensionPropertiesApi.md). +* `CodeString` – A value class wrapper for `String`, containing + the generated Kotlin code of `@DataSchema` interfaces. + It also contains generated [extension properties](extensionPropertiesApi.md) + for `extensionProperties=true`. -### Examples {id="generateDataClasses-examples"} +### Examples {id="generateInterfaces-examples"} - + ```kotlin -df.generateDataClasses("Customer") +df ``` -Output: + + + ```kotlin -@DataSchema -data class Customer1( - val amount: Double, - val orderId: Int -) +df.generateInterfaces(markerName = "Customer") +``` + +Output: +```kotlin @DataSchema -data class Customer( - val orders: List, +interface Customer { + val orders: List val user: String -) + + @DataSchema(isOpen = false) + interface Orders { + val amount: Double + val orderId: Int + } +} ``` -Add these classes to your project and convert the DataFrame to a list of typed objects: + - +By adding these interfaces to your project with the [compiler plugin](Compiler-Plugin.md) enabled, +you'll gain full support for the [extension properties API](extensionPropertiesApi.md) and type-safe operations. + +Use [`cast`](cast.md) to apply the generated schema to a `DataFrame`. +Right after that, you can use [extension properties](extensionPropertiesApi.md) +in the following operations: + + ```kotlin -val customers: List = df.cast().toList() +df.cast().filter { orders.all { orderId >= 102 } } ``` -## generateInterfaces +## generateDataClasses ```kotlin -inline fun DataFrame.generateInterfaces(): CodeString - -fun DataFrame.generateInterfaces(markerName: String): CodeString +inline fun DataFrame.generateDataClasses( + markerName: String? = null, + extensionProperties: Boolean = false, + visibility: MarkerVisibility = MarkerVisibility.IMPLICIT_PUBLIC, + useFqNames: Boolean = false, + nameNormalizer: NameNormalizer = NameNormalizer.default, + nestedMarkerNameProvider: MarkerNameProvider = MarkerNameProvider.fromColumnName, +): CodeString ``` -Generates [`@DataSchema`](schemas.md) interfaces for this `DataFrame` -(including all nested `DataFrame` columns and column groups) as Kotlin interfaces. +Generates Kotlin data classes corresponding to the `DataFrame` schema +(including all nested `DataFrame` columns and column groups). -This is useful when working with the [compiler plugin](Compiler-Plugin.md) -in cases where the schema cannot be inferred automatically from the source. +Useful when you want to: -### Arguments {id="generateInterfaces-arguments"} +- Work with the data as regular Kotlin data classes. +- Convert a dataframe to instantiated data classes with `df.toListOf()`. +- Work with data classes serialization. +- Extract structured types for further use in your application. -* `markerName`: `String?` — The base name to use for generated interfaces. +### Arguments {id="generateDataClasses-arguments"} + +* `markerName`: `String?` — The base name to use for generated data classes. If `null`, uses the `T` type argument of `DataFrame` simple name. Default: `null`. * `extensionProperties`: `Boolean` – Whether to generate [extension properties](extensionPropertiesApi.md) @@ -143,60 +160,60 @@ in cases where the schema cannot be inferred automatically from the source. - `MarkerNameProvider.PredefinedName` uses the name of the root marker for all nested declarations and appends a numerical suffix to resolve name conflicts. Default: `MarkerNameProvider.fromColumnName`. -### Returns {id="generateInterfaces-returns"} +### Returns {id="generateDataClasses-returns"} * `CodeString` – A value class wrapper for `String`, containing - the generated Kotlin code of `@DataSchema` interfaces without [extension properties](extensionPropertiesApi.md). - -### Examples {id="generateInterfaces-examples"} - - + the generated Kotlin code of `@DataSchema` data classes. + It also contains generated [extension properties](extensionPropertiesApi.md) + for `extensionProperties=true`. -```kotlin -df -``` - - - - +### Examples {id="generateDataClasses-examples"} - + ```kotlin -df.generateInterfaces() +df.generateDataClasses("Customer") ``` - - Output: ```kotlin -@DataSchema(isOpen = false) -interface _DataFrameType11 { - val amount: kotlin.Double - val orderId: kotlin.Int -} - @DataSchema -interface _DataFrameType1 { - val orders: List<_DataFrameType11> - val user: kotlin.String +data class Customer( + val orders: List, + val user: String +) { + @DataSchema + data class Orders( + val amount: Double, + val orderId: Int + ) } ``` + + By adding these interfaces to your project with the [compiler plugin](Compiler-Plugin.md) enabled, you'll gain full support for the [extension properties API](extensionPropertiesApi.md) and type-safe operations. -Use [`cast`](cast.md) to apply the generated schema to a `DataFrame`: +Use [`cast`](cast.md) to apply the generated schema to a `DataFrame`. +Right after that, you can use [extension properties](extensionPropertiesApi.md) +in the following operations: ```kotlin -df.filter { orders.all { orderId >= 102 } } +df.cast().filter { orders.all { orderId >= 102 } } ``` - +You can use these classes for the `DataFrame` conversion to a list of typed objects: + +```kotlin +val customers: List = df.cast().toList() +``` + + diff --git a/docs/StardustDocs/topics/schemas/Migration-From-Plugins.md b/docs/StardustDocs/topics/schemas/Migration-From-Plugins.md index cd09c5247d..04591b99d8 100644 --- a/docs/StardustDocs/topics/schemas/Migration-From-Plugins.md +++ b/docs/StardustDocs/topics/schemas/Migration-From-Plugins.md @@ -1,7 +1,12 @@ # Migration from Gradle/KSP Plugin Gradle and KSP plugins were useful tools in earlier versions of Kotlin DataFrame. -However, they are now being phased out. This section provides an overview of their current state and migration guidance. +However, they are now **deprecated**. +Their latest release is 1.0.0-Beta4 and will not have future releases. +This page provides migration guidance. + +Our plans for the next iteration of schema generation from source feature can be found in +[Issue #1844](https://github.com/Kotlin/dataframe/issues/1844). ## Gradle Plugin @@ -20,16 +25,6 @@ However, they are now being phased out. This section provides an overview of the - Generates extension properties for declared data schemas. - Automatically updates the schema and regenerates properties after structural DataFrame operations. -> The Gradle plugin still works and may be helpful for generating schemas from data sources. -> However, it is planned for deprecation, and **we do not recommend using it going forward**. -> {style="warning"} - -If you still choose to use Gradle plugin, make sure to disable the automatic KSP plugin dependency -to avoid compatibility issues with Kotlin 2.1+ by adding this line to `gradle.properties`: - -```properties -kotlin.dataframe.add.ksp=false -``` ## KSP Plugin @@ -38,16 +33,3 @@ kotlin.dataframe.add.ksp=false - You could copy already generated schemas from `build/generate/ksp` into your project sources. - To generate a `DataSchema` for a [`DataFrame`](DataFrame.md) now, use the [`generate..()` methods](DataSchemaGenerationMethods.md) instead. - -> The KSP plugin is **not compatible with [KSP2](https://github.com/google/ksp?tab=readme-ov-file#ksp2-is-here)** -> and may **not work properly with Kotlin 2.1 or newer**. -> It is planned for deprecation or major changes, and **we do not recommend using it at this time**. -> {style="warning"} - -If you still choose to use the KSP plugin with Kotlin 2.1+, -disable [KSP2](https://github.com/google/ksp?tab=readme-ov-file#ksp2-is-here) -by adding this line to `gradle.properties`: - -```properties -ksp.useKSP2=false -``` diff --git a/docs/StardustDocs/topics/schemas/schemas.md b/docs/StardustDocs/topics/schemas/schemas.md index 4a2328c500..6390112a27 100644 --- a/docs/StardustDocs/topics/schemas/schemas.md +++ b/docs/StardustDocs/topics/schemas/schemas.md @@ -1,20 +1,42 @@ [//]: # (title: Data Schemas) + +Define, generate, and use typed data schemas in Kotlin DataFrame with `@DataSchema`, +compiler plugin support, and extension property generation for safer dataframe operations. + + + +Learn about data schemas, which provide typed access to dataframe columns through generated extension +properties, including support for hierarchical and nested dataframe structures. + + + +Typed dataframe schemas in Kotlin DataFrame — define schemas with `@DataSchema`, +generate extension properties and work safely with structured and nested data. + + The Kotlin DataFrame library provides typed data access via [generation of extension properties](extensionPropertiesApi.md) for the type -[`DataFrame`](DataFrame.md) (as well as for [`DataRow`](DataRow.md)), where +[`DataFrame`](DataFrame.md) (as well as for [`DataRow`](DataRow.md) +and [`ColumnGroup`](DataColumn.md#columngroup)), where `T` is a marker class representing the `DataSchema` of the [`DataFrame`](DataFrame.md). A *schema* of a [`DataFrame`](DataFrame.md) is a mapping from column names to column types. -This data schema can be expressed as a Kotlin class or interface. -If the DataFrame is hierarchical — contains a [column group](DataColumn.md#columngroup) or a +This data schema can be expressed as a Kotlin interface or data class by annotating it with `@DataSchema`. +If the dataframe is hierarchical — contains a [column group](DataColumn.md#columngroup) or a [column of dataframes](DataColumn.md#framecolumn) — the data schema reflects this structure, with a separate class representing the schema of each column group or nested `DataFrame`. -For example, consider a simple hierarchical DataFrame from +For example, consider a simple hierarchical dataframe from . -This DataFrame consists of two columns: +> Note that this is not a regular CSV file — it contains a column with embedded JSON values. +> +> To read such files correctly, both the [`dataframe-csv`](Modules.md#dataframe-csv) +> and [`dataframe-json`](Modules.md#dataframe-json) modules must be included. +> {style="note"} + +This dataframe consists of two columns: - `name`, which is a `String` column - `info`, which is a [column group](DataColumn.md#columngroup) containing two nested [value columns](DataColumn.md#valuecolumn): - `age` of type `Int` @@ -46,22 +68,22 @@ This DataFrame consists of two columns:
-The data schema corresponding to this DataFrame can be represented as: +The data schema corresponding to this `DataFrame` can be represented as: ```kotlin // Data schema of the "info" column group @DataSchema -data class Info( - val age: Int, +interface Info { + val age: Int val height: Float -) +} // Data schema of the entire DataFrame @DataSchema -data class Person( - val info: Info, +interface Person { + val info: Info val name: String -) +} ``` [Extension properties](extensionPropertiesApi.md) for `DataFrame` @@ -83,8 +105,38 @@ df.filter { age >= 18 } See [](extensionPropertiesApi.md) for more information. +## `@DataSchema` annotation -## Schema Retrieving +`@DataSchema` is a Kotlin annotation that marks a data class or interface as a data schema. +[The compiler plugin](Compiler-Plugin.md) generates [extension properties](extensionPropertiesApi.md) for the `DataFrame` +(or [`DataRow`](DataRow.md), [`ColumnGroup`](DataColumn.md#columngroup), etc.) +with a type parameter annotated with `@DataSchema`. + +> While you can annotate any Kotlin class or object with a `@DataSchema`, +> we highly recommend using it only on interfaces and data classes specially made +> for representing the data schema of a `DataFrame`. +> +> Use only trivial properties, avoiding computed, `lateinit`, or delegated properties. +> In data classes, provide only constructor properties. +> +> In all other cases, the behavior may be undefined. +> If you do need to use `@DataSchema` on a “complex” class, please let us know via +> the [issues](https://github.com/Kotlin/dataframe/issues). +{style="warning"} + +Each property of an annotated class or interface corresponds to a column in the `DataFrame` +(or [`DataRow`](DataRow.md), [`ColumnGroup`](DataColumn.md#columngroup), etc.). +The property name is the column name, and the property type is the column type. + +> Data schema is considered *compatible* if it contains **any subset** of actual dataframe columns +> with correct types. +> If the data schema contains columns that are not present in the dataframe, +> it is considered *incompatible*. +> +> This is checked in [`.cast()`](cast.md) with `verify=true` and [`.convertTo()`](convertTo.md) methods. +{style="warning"} + +## Data Schema Retrieving Defining a data schema manually can be difficult, especially for dataframes with many columns or deeply nested structures, and may lead to mistakes in column names or types. @@ -107,11 +159,8 @@ It will also **automatically update** the schema during operations that modify t ### Plugins -> The current Gradle plugin is **under consideration for deprecation** and -> may be officially marked as deprecated in future releases. -> -> The KSP plugin is **not compatible with [KSP2](https://github.com/google/ksp?tab=readme-ov-file#ksp2-is-here)** -> and may **not work properly with Kotlin 2.1 or newer**. +> The current Gradle and KSP plugins are **deprecated**. +> Their latest release is 1.0.0-Beta4 and will not have future releases. > > At the moment, **[data schema generation is handled via dedicated methods](DataSchemaGenerationMethods.md)** instead of relying on the plugins. {style="warning"} @@ -122,6 +171,14 @@ It will also **automatically update** the schema during operations that modify t [Kotlin Symbol Processing](https://kotlinlang.org/docs/ksp-overview.html) by specifying a source file path in your code file. +## Specifying Data Schema + +To bring the `DataFrame` into the desired schema, you can use one of two operations: + +* Specify the schema using [`cast`](cast.md). +* Convert the `DataFrame` to the target schema using [`convertTo`](convertTo.md). + + ## Extension Properties Generation Once you have a data schema, you can generate [extension properties](extensionPropertiesApi.md). @@ -145,3 +202,24 @@ See [extension properties example in Kotlin Notebook](extensionPropertiesApi.md# [extension properties](extensionPropertiesApi.md) for a [`DataFrame`](DataFrame.md) manually by calling one of the [`generate..()` methods](DataSchemaGenerationMethods.md) with the `extensionProperties = true` argument. + +### Custom extension properties + +Sometimes it is also useful to define your own extension properties +based on a [data schema](schema.md). + +```kotlin +@DataSchema +interface BranchData { + val expenses: Long + val revenue: Long +} + +val DataRow.profit get() = revenue - expenses +``` + +```kotlin +val dfProfitable = df.filter { it.profit > 0 } +``` + +See [](extensionPropertiesApi.md#custom-extension-properties) for more information. diff --git a/samples/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/Generate.kt b/samples/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/Generate.kt index 575c3b6528..fe74c44154 100644 --- a/samples/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/Generate.kt +++ b/samples/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/Generate.kt @@ -2,6 +2,7 @@ package org.jetbrains.kotlinx.dataframe.samples.api +import org.jetbrains.kotlinx.dataframe.AnyFrame import org.jetbrains.kotlinx.dataframe.annotations.DataSchema import org.jetbrains.kotlinx.dataframe.api.add import org.jetbrains.kotlinx.dataframe.api.all @@ -20,31 +21,30 @@ import org.junit.Test class Generate : DataFrameSampleHelper("generate_docs", "api") { @DataSchema - data class Orders( - val orderId: Int, - val amount: Double, - ) - + data class Customer( + val orders: List, + val user: String + ) { + @DataSchema + data class Orders( + val amount: Double, + val orderId: Int + ) + } private val ordersAlice = dataFrameOf( "orderId" to listOf(101, 102), "amount" to listOf(50.0, 75.5), - ).cast() + ).cast() private val ordersBob = dataFrameOf( "orderId" to listOf(103, 104, 105), "amount" to listOf(20.0, 30.0, 25.0), - ).cast() - - @DataSchema - data class Customer( - val user: String, - val orders: List, - ) + ).cast() - private val df = dataFrameOf( + private val df: AnyFrame = dataFrameOf( "user" to listOf("Alice", "Bob"), "orders" to listOf(ordersAlice, ordersBob), - ).cast() + ) @Test fun notebook_test_generate_docs_1() { @@ -57,14 +57,15 @@ class Generate : DataFrameSampleHelper("generate_docs", "api") { @Test fun notebook_test_generate_docs_2() { // SampleStart - df.generateInterfaces() + df.generateInterfaces(markerName = "Customer") // SampleEnd + .saveSample() } @Test fun notebook_test_generate_docs_3() { // SampleStart - df.filter { orders.all { orderId >= 102 } } + df.cast().filter { orders.all { orderId >= 102 } } // SampleEnd // .saveDfHtmlSample() } @@ -74,6 +75,7 @@ class Generate : DataFrameSampleHelper("generate_docs", "api") { // SampleStart df.generateDataClasses("Customer") // SampleEnd + .saveSample() } @Test @@ -88,6 +90,7 @@ class Generate : DataFrameSampleHelper("generate_docs", "api") { // SampleStart df.generateInterfaces(markerName = "Customer") // SampleEnd + .saveSample() } @Test