This is an automated email from the ASF dual-hosted git repository.

psxjoy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fesod.git


The following commit(s) were added to refs/heads/main by this push:
     new d7fae5a4 feat: add fluent header api for no-bean mode (#943)
d7fae5a4 is described below

commit d7fae5a40f5a537582663df86c50cf413154a5f6
Author: Bengbengbalabalabeng 
<[email protected]>
AuthorDate: Wed Jul 8 19:44:17 2026 +0800

    feat: add fluent header api for no-bean mode (#943)
    
    * feat: add fluent header api for no-bean mode
    
    * docs: refine method summary Javadoc for head(List), head(Consumer), and 
head(Class).
---
 .../sheet/metadata/AbstractParameterBuilder.java   |  40 +++-
 .../fesod/sheet/metadata/DefaultHeadBuilder.java   | 133 +++++++++++++
 .../apache/fesod/sheet/metadata/HeadBuilder.java   | 134 +++++++++++++
 .../fesod/sheet/metadata/HeadBuilderTest.java      | 219 +++++++++++++++++++++
 4 files changed, 520 insertions(+), 6 deletions(-)

diff --git 
a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractParameterBuilder.java
 
b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractParameterBuilder.java
index e2966ea5..de25c3c0 100644
--- 
a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractParameterBuilder.java
+++ 
b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractParameterBuilder.java
@@ -29,6 +29,7 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
 import java.util.Objects;
+import java.util.function.Consumer;
 import org.apache.fesod.common.util.ListUtils;
 import org.apache.fesod.sheet.converters.Converter;
 import org.apache.fesod.sheet.enums.CacheLocationEnum;
@@ -39,17 +40,39 @@ import org.apache.fesod.sheet.enums.CacheLocationEnum;
  *
  */
 public abstract class AbstractParameterBuilder<T extends 
AbstractParameterBuilder, C extends BasicParameter> {
+
     /**
-     * You can only choose one of the {@link #head(List)} and {@link 
#head(Class)}
+     * Configure sheet headers dynamically using a raw {@code 
List<List<String>>}.
      *
-     * @param head
-     * @return
+     * <p>
+     * <strong>Note:</strong> Use of this method is mutually exclusive with 
{@link #head(Class)}.
+     * </p>
+     *
+     * @param head the raw header data list
+     * @see #head(Consumer)
+     * @return this builder
      */
     public T head(List<List<String>> head) {
         parameter().setHead(toMutableListIfNecessary(head));
         return self();
     }
 
+    /**
+     * Configure sheet headers dynamically using a {@code HeadBuilder} 
consumer.
+     *
+     * <p>
+     * <strong>Note:</strong> Use of this method is mutually exclusive with 
{@link #head(Class)}.
+     * </p>
+     *
+     * @param headBuilderConsumer the consumer to configure the headers
+     * @see #head(List)
+     * @return this builder
+     */
+    public T head(Consumer<HeadBuilder> headBuilderConsumer) {
+        parameter().setHead(DefaultHeadBuilder.define(headBuilderConsumer));
+        return self();
+    }
+
     /**
      * Ensures and returns a mutable head list.
      *
@@ -68,10 +91,15 @@ public abstract class AbstractParameterBuilder<T extends 
AbstractParameterBuilde
     }
 
     /**
-     * You can only choose one of the {@link #head(List)} and {@link 
#head(Class)}
+     * Configure sheet headers using a Java model.
      *
-     * @param clazz
-     * @return
+     * <p>
+     * <strong>Note:</strong> Use of this method is mutually exclusive with
+     * {@link #head(List)} and {@link #head(Consumer)}.
+     * </p>
+     *
+     * @param clazz the Java model class
+     * @return this builder
      */
     public T head(Class<?> clazz) {
         parameter().setClazz(clazz);
diff --git 
a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/DefaultHeadBuilder.java
 
b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/DefaultHeadBuilder.java
new file mode 100644
index 00000000..dec891cf
--- /dev/null
+++ 
b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/DefaultHeadBuilder.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fesod.sheet.metadata;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+import org.apache.commons.lang3.Validate;
+
+/**
+ * Standard implementation of {@code HeadBuilder}.
+ */
+class DefaultHeadBuilder implements HeadBuilder {
+
+    private final List<List<String>> columns;
+    private final List<String> prefixes;
+
+    DefaultHeadBuilder() {
+        this(new ArrayList<>(), new ArrayList<>());
+    }
+
+    DefaultHeadBuilder(List<List<String>> columns, List<String> prefixes) {
+        this.columns = columns;
+        this.prefixes = prefixes;
+    }
+
+    static List<List<String>> define(Consumer<HeadBuilder> 
headBuilderConsumer) {
+        Validate.notNull(headBuilderConsumer, "headBuilderConsumer must not be 
null");
+
+        DefaultHeadBuilder builder = new DefaultHeadBuilder();
+        headBuilderConsumer.accept(builder);
+        return builder.toHead();
+    }
+
+    /**
+     * Define a single column with a fixed head names (containing at least one 
column).
+     *
+     * @param headName the first header name (must not be {@code null})
+     * @param subHeadNames optional sublevel header names
+     * @return this for builder chains
+     */
+    @Override
+    public HeadBuilder column(String headName, String... subHeadNames) {
+        Validate.notNull(headName, "header name must not be null");
+
+        int initialCapacity = prefixes.size() + ((subHeadNames == null) ? 0 : 
subHeadNames.length) + 1;
+        List<String> current = new ArrayList<>(initialCapacity);
+        current.addAll(prefixes);
+        current.add(headName);
+
+        if (subHeadNames != null) {
+            Validate.noNullElements(subHeadNames, "sub-header names must not 
contain null elements");
+            Collections.addAll(current, subHeadNames);
+        }
+        columns.add(current);
+        return this;
+    }
+
+    /**
+     * Define a single column with a repeating header name at multiple levels.
+     *
+     * @param headName the header name to repeat (must not be {@code null})
+     * @param repeat the number of times to repeat the name
+     * @return this for builder chains
+     */
+    @Override
+    public HeadBuilder column(String headName, int repeat) {
+        Validate.notNull(headName, "header name must not be null");
+        Validate.isTrue(repeat > 0, "header repeat must be greater than 0");
+
+        int initialCapacity = prefixes.size() + repeat;
+        List<String> current = new ArrayList<>(initialCapacity);
+        current.addAll(prefixes);
+
+        for (int i = 0; i < repeat; i++) {
+            current.add(headName);
+        }
+
+        columns.add(current);
+        return this;
+    }
+
+    /**
+     * Declare multiple shared parent header names to group sub-headers.
+     *
+     * @param parentHeadNames the shared parent header names (must not be 
{@code null} or empty)
+     * @param subHeadBuilderConsumer the consumer to define sub-headers
+     * @return this for builder chains
+     */
+    @Override
+    public HeadBuilder columns(List<String> parentHeadNames, 
Consumer<HeadBuilder> subHeadBuilderConsumer) {
+        Validate.notEmpty(parentHeadNames, "parent header names must not be 
null or empty");
+        Validate.noNullElements(parentHeadNames, "parent header names must not 
contain null elements");
+        Validate.notNull(subHeadBuilderConsumer, "subHeadBuilderConsumer must 
not be null");
+
+        int previousSize = this.prefixes.size();
+        this.prefixes.addAll(parentHeadNames);
+
+        subHeadBuilderConsumer.accept(this);
+
+        while (this.prefixes.size() > previousSize) {
+            this.prefixes.remove(this.prefixes.size() - 1);
+        }
+        return this;
+    }
+
+    /**
+     * Return the heads.
+     *
+     * @return the list of head
+     */
+    List<List<String>> toHead() {
+        return columns;
+    }
+}
diff --git 
a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/HeadBuilder.java 
b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/HeadBuilder.java
new file mode 100644
index 00000000..39b3984a
--- /dev/null
+++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/HeadBuilder.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fesod.sheet.metadata;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+import org.apache.commons.lang3.Validate;
+
+/**
+ * A builder for configuring sheet headers in No-Bean mode.
+ *
+ * <p>Example - complex headers:</p>
+ * <pre>
+ * {@code FesodSheet.write(pathname)
+ *           .head(builder -> builder
+ *              .column("ID", 2)
+ *              .columns("User Info", sub -> sub.column("Name").column("Age"))
+ *              .column("Others", "Remark")
+ *           )
+ * // Equivalent to:
+ * List<List<String>> head = new ArrayList<>();
+ * head.add(new ArrayList<>(Arrays.asList("ID", "ID")));
+ * head.add(new ArrayList<>(Arrays.asList("User Info", "Name")));
+ * head.add(new ArrayList<>(Arrays.asList("User Info", "Age")));
+ * head.add(new ArrayList<>(Arrays.asList("Others", "Remark")));
+ *
+ * FesodSheet.write(pathname)
+ *           .head(head)}
+ * </pre>
+ *
+ * <p>Example - single-level headers:</p>
+ * <pre>
+ * {@code FesodSheet.write(pathname)
+ *           .head(HeadBuilder.forSimple("ID", "Name", "Age", "Remark"))
+ * // Equivalent to:
+ * List<List<String>> head = new ArrayList<>();
+ * head.add(new ArrayList<>(Arrays.asList("ID")));
+ * head.add(new ArrayList<>(Arrays.asList("Name")));
+ * head.add(new ArrayList<>(Arrays.asList("Age")));
+ * head.add(new ArrayList<>(Arrays.asList("Remark")));
+ *
+ * FesodSheet.write(pathname)
+ *     .head(head)}
+ * </pre>
+ *
+ * @see AbstractParameterBuilder#head(Consumer)
+ */
+public interface HeadBuilder {
+
+    /**
+     * Define a single column with a fixed head names (containing at least one 
column).
+     *
+     * @param headName the first header name (must not be {@code null})
+     * @param subHeadNames optional sublevel header names
+     * @return this for builder chains
+     */
+    HeadBuilder column(String headName, String... subHeadNames);
+
+    /**
+     * Define a single column with a repeating header name at multiple levels.
+     *
+     * @param headName the header name to repeat (must not be {@code null})
+     * @param repeat the number of times to repeat the name
+     * @return this for builder chains
+     */
+    HeadBuilder column(String headName, int repeat);
+
+    /**
+     * Declare a shared parent header name to group sub-headers.
+     *
+     * @param parentHeadName the shared parent header name (must not be {@code 
null})
+     * @param subHeadBuilderConsumer the consumer to define sub-headers
+     * @return this for builder chains
+     */
+    default HeadBuilder columns(String parentHeadName, Consumer<HeadBuilder> 
subHeadBuilderConsumer) {
+        return columns(Collections.singletonList(parentHeadName), 
subHeadBuilderConsumer);
+    }
+
+    /**
+     * Declare multiple shared parent header names to group sub-headers.
+     *
+     * @param parentHeadNames  the shared parent header names (must not be 
{@code null} or empty)
+     * @param subHeadBuilderConsumer the consumer to define sub-headers
+     * @return this for builder chains
+     */
+    HeadBuilder columns(List<String> parentHeadNames, Consumer<HeadBuilder> 
subHeadBuilderConsumer);
+
+    /**
+     * Build a simple single-level header list (containing at least one 
column).
+     *
+     * @param headName the first header name (must not be {@code null})
+     * @param otherHeadNames optional other header names
+     * @return a simple single-level header list
+     */
+    static List<List<String>> forSimple(String headName, String... 
otherHeadNames) {
+        Validate.notNull(headName, "header name must not be null");
+
+        int initialCapacity = ((otherHeadNames == null) ? 0 : 
otherHeadNames.length) + 1;
+        List<List<String>> result = new ArrayList<>(initialCapacity);
+
+        List<String> firstHead = new ArrayList<>(1);
+        firstHead.add(headName);
+        result.add(firstHead);
+
+        if (otherHeadNames != null) {
+            Validate.noNullElements(otherHeadNames, "other header names must 
not contain null elements");
+            for (String otherHeadName : otherHeadNames) {
+                List<String> otherHead = new ArrayList<>(1);
+                otherHead.add(otherHeadName);
+                result.add(otherHead);
+            }
+        }
+        return result;
+    }
+}
diff --git 
a/fesod-sheet/src/test/java/org/apache/fesod/sheet/metadata/HeadBuilderTest.java
 
b/fesod-sheet/src/test/java/org/apache/fesod/sheet/metadata/HeadBuilderTest.java
new file mode 100644
index 00000000..d0d15590
--- /dev/null
+++ 
b/fesod-sheet/src/test/java/org/apache/fesod/sheet/metadata/HeadBuilderTest.java
@@ -0,0 +1,219 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fesod.sheet.metadata;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+import org.apache.fesod.sheet.testkit.Tags;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests {@link DefaultHeadBuilder} and {@link HeadBuilder}.
+ */
+@Tag(Tags.UNIT)
+class HeadBuilderTest {
+
+    static Consumer<HeadBuilder> withEmpty() {
+        return b -> {};
+    }
+
+    @Test
+    void forSimple_singleName() {
+        List<List<String>> head = HeadBuilder.forSimple("ID");
+
+        Assertions.assertEquals(1, head.size());
+        Assertions.assertEquals(Arrays.asList("ID"), head.get(0));
+    }
+
+    @Test
+    void forSimple_multipleNames() {
+        List<List<String>> head = HeadBuilder.forSimple("ID", "Name", "Age");
+
+        Assertions.assertEquals(3, head.size());
+        Assertions.assertEquals(Arrays.asList("ID"), head.get(0));
+        Assertions.assertEquals(Arrays.asList("Name"), head.get(1));
+        Assertions.assertEquals(Arrays.asList("Age"), head.get(2));
+    }
+
+    @Test
+    void forSimple_nullFirstNameThrows() {
+        Assertions.assertThrows(NullPointerException.class, () -> 
HeadBuilder.forSimple(null, "Name"));
+    }
+
+    @Test
+    void forSimple_nullOtherNameThrows() {
+        Assertions.assertThrows(IllegalArgumentException.class, () -> 
HeadBuilder.forSimple("ID", "Name", null));
+    }
+
+    @Test
+    void define_nullConsumerThrows() {
+        Assertions.assertThrows(NullPointerException.class, () -> 
DefaultHeadBuilder.define(null));
+    }
+
+    @Test
+    void define_emptyConsumerReturnsEmptyHead() {
+        List<List<String>> head = DefaultHeadBuilder.define(withEmpty());
+
+        Assertions.assertNotNull(head);
+        Assertions.assertTrue(head.isEmpty());
+    }
+
+    @Test
+    void column_singleName() {
+        List<List<String>> head = DefaultHeadBuilder.define(b -> 
b.column("ID"));
+
+        Assertions.assertEquals(1, head.size());
+        Assertions.assertEquals(Arrays.asList("ID"), head.get(0));
+    }
+
+    @Test
+    void column_withSubHeadNames() {
+        List<List<String>> head = DefaultHeadBuilder.define(b -> b.column("A", 
"B", "C"));
+
+        Assertions.assertEquals(1, head.size());
+        Assertions.assertEquals(Arrays.asList("A", "B", "C"), head.get(0));
+    }
+
+    @Test
+    void column_nullHeadNameThrows() {
+        Assertions.assertThrows(NullPointerException.class, () -> 
DefaultHeadBuilder.define(b -> b.column(null, "B")));
+    }
+
+    @Test
+    void column_nullSubHeadNameThrows() {
+        Assertions.assertThrows(
+                IllegalArgumentException.class, () -> 
DefaultHeadBuilder.define(b -> b.column("A", "B", null)));
+    }
+
+    @Test
+    void column_repeatOnce() {
+        List<List<String>> head = DefaultHeadBuilder.define(b -> b.column("X", 
1));
+
+        Assertions.assertEquals(Arrays.asList("X"), head.get(0));
+    }
+
+    @Test
+    void column_repeatMultiple() {
+        List<List<String>> head = DefaultHeadBuilder.define(b -> b.column("X", 
3));
+
+        Assertions.assertEquals(1, head.size());
+        Assertions.assertEquals(Arrays.asList("X", "X", "X"), head.get(0));
+    }
+
+    @Test
+    void column_repeatZeroThrows() {
+        Assertions.assertThrows(IllegalArgumentException.class, () -> 
DefaultHeadBuilder.define(b -> b.column("X", 0)));
+    }
+
+    @Test
+    void column_repeatNegativeThrows() {
+        Assertions.assertThrows(
+                IllegalArgumentException.class, () -> 
DefaultHeadBuilder.define(b -> b.column("X", -2)));
+    }
+
+    @Test
+    void column_repeat_nullHeadNameThrows() {
+        Assertions.assertThrows(NullPointerException.class, () -> 
DefaultHeadBuilder.define(b -> b.column(null, 2)));
+    }
+
+    @Test
+    void columns_singleParentAppliesPrefixToSubColumns() {
+        List<List<String>> head = DefaultHeadBuilder.define(
+                b -> b.columns("User Info", sub -> 
sub.column("Name").column("Age")));
+
+        Assertions.assertEquals(2, head.size());
+        Assertions.assertEquals(Arrays.asList("User Info", "Name"), 
head.get(0));
+        Assertions.assertEquals(Arrays.asList("User Info", "Age"), 
head.get(1));
+    }
+
+    @Test
+    void columns_multipleParentNamesStackAsPrefix() {
+        List<List<String>> head = DefaultHeadBuilder.define(
+                b -> b.columns(Arrays.asList("P", "Q"), sub -> 
sub.column("A").column("B")));
+
+        Assertions.assertEquals(2, head.size());
+        Assertions.assertEquals(Arrays.asList("P", "Q", "A"), head.get(0));
+        Assertions.assertEquals(Arrays.asList("P", "Q", "B"), head.get(1));
+    }
+
+    @Test
+    void columns_prefixesAreRestoredAfterBlock() {
+        List<List<String>> head = DefaultHeadBuilder.define(
+                b -> b.columns("P", sub -> sub.column("A")).column("B"));
+
+        Assertions.assertEquals(2, head.size());
+        Assertions.assertEquals(Arrays.asList("P", "A"), head.get(0));
+        Assertions.assertEquals(Arrays.asList("B"), head.get(1));
+    }
+
+    @Test
+    void columns_nestedParentsConcatenate() {
+        List<List<String>> head = DefaultHeadBuilder.define(
+                b -> b.columns("Outer", outer -> outer.columns("Inner", inner 
-> inner.column("Leaf"))));
+
+        Assertions.assertEquals(1, head.size());
+        Assertions.assertEquals(Arrays.asList("Outer", "Inner", "Leaf"), 
head.get(0));
+    }
+
+    @Test
+    void columns_emptyParentListIsNotAllowed() {
+        Assertions.assertThrows(
+                IllegalArgumentException.class,
+                () -> DefaultHeadBuilder.define(b -> 
b.columns(Collections.emptyList(), sub -> sub.column("A"))));
+    }
+
+    @Test
+    void columns_nullParentNamesThrows() {
+        Assertions.assertThrows(
+                NullPointerException.class,
+                () -> DefaultHeadBuilder.define(b -> b.columns((List<String>) 
null, withEmpty())));
+    }
+
+    @Test
+    void columns_nullElementInParentNamesThrows() {
+        Assertions.assertThrows(
+                IllegalArgumentException.class,
+                () -> DefaultHeadBuilder.define(b -> 
b.columns(Arrays.asList("P", null), withEmpty())));
+    }
+
+    @Test
+    void columns_nullConsumerThrows() {
+        Assertions.assertThrows(
+                NullPointerException.class,
+                () -> DefaultHeadBuilder.define(b -> 
b.columns(Arrays.asList("P"), (Consumer<HeadBuilder>) null)));
+    }
+
+    @Test
+    void complexExampleFromJavadoc() {
+        List<List<String>> head = DefaultHeadBuilder.define(b -> 
b.column("ID", 2)
+                .columns("User Info", sub -> sub.column("Name").column("Age"))
+                .column("Others", "Remark"));
+
+        Assertions.assertEquals(4, head.size());
+        Assertions.assertEquals(Arrays.asList("ID", "ID"), head.get(0));
+        Assertions.assertEquals(Arrays.asList("User Info", "Name"), 
head.get(1));
+        Assertions.assertEquals(Arrays.asList("User Info", "Age"), 
head.get(2));
+        Assertions.assertEquals(Arrays.asList("Others", "Remark"), 
head.get(3));
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to