001package com.streamconverter.command.rule.impl.casing;
002
003import com.streamconverter.command.rule.IRule;
004import java.util.regex.Matcher;
005import java.util.regex.Pattern;
006
007/**
008 * Transforms snake_case strings to camelCase format.
009 *
010 * <p>This rule converts strings from snake_case notation to camelCase notation by removing
011 * underscores and capitalizing the first letter of each word after the first.
012 *
013 * <p>Examples:
014 *
015 * <ul>
016 *   <li>{@code user_name} → {@code userName}
017 *   <li>{@code first_name} → {@code firstName}
018 *   <li>{@code user_account_id} → {@code userAccountId}
019 *   <li>{@code xml_http_request} → {@code xmlHttpRequest}
020 * </ul>
021 *
022 * <p>Usage:
023 *
024 * <pre>{@code
025 * IRule rule = SnakeToCamelCaseRule.builder().build();
026 * IStreamCommand command = JsonNavigateCommand.create("$.user_name", rule);
027 * }</pre>
028 *
029 * @since 1.0
030 */
031public class SnakeToCamelCaseRule implements IRule {
032
033  /** Pattern to match snake_case transitions (underscore followed by letter) */
034  private static final Pattern SNAKE_CASE_PATTERN = Pattern.compile("_([a-z])");
035
036  /** Pattern to match multiple consecutive underscores */
037  private static final Pattern MULTIPLE_UNDERSCORES_PATTERN = Pattern.compile("_{2,}");
038
039  /** Whether to capitalize the first letter (PascalCase instead of camelCase) */
040  private final boolean capitalizeFirst;
041
042  /** Whether to preserve leading/trailing underscores */
043  private final boolean preserveUnderscores;
044
045  /** Private constructor for builder pattern */
046  private SnakeToCamelCaseRule(boolean capitalizeFirst, boolean preserveUnderscores) {
047    this.capitalizeFirst = capitalizeFirst;
048    this.preserveUnderscores = preserveUnderscores;
049  }
050
051  @Override
052  public String apply(String input) {
053    if (input == null || input.isEmpty()) {
054      return input;
055    }
056
057    String result = input;
058
059    // Clean up multiple underscores if not preserving
060    if (!preserveUnderscores) {
061      result = MULTIPLE_UNDERSCORES_PATTERN.matcher(result).replaceAll("_");
062    }
063
064    // Convert snake_case to camelCase by capitalizing letters after underscores
065    Matcher matcher = SNAKE_CASE_PATTERN.matcher(result);
066    StringBuffer sb = new StringBuffer();
067
068    while (matcher.find()) {
069      String replacement = matcher.group(1).toUpperCase();
070      matcher.appendReplacement(sb, replacement);
071    }
072    matcher.appendTail(sb);
073    result = sb.toString();
074
075    // Handle leading/trailing underscores
076    if (!preserveUnderscores) {
077      result = result.replaceAll("^_+|_+$", "");
078    }
079
080    // Capitalize first letter if PascalCase is requested
081    if (capitalizeFirst && !result.isEmpty()) {
082      result = Character.toUpperCase(result.charAt(0)) + result.substring(1);
083    } else if (!capitalizeFirst && !result.isEmpty()) {
084      // Ensure first letter is lowercase for camelCase
085      result = Character.toLowerCase(result.charAt(0)) + result.substring(1);
086    }
087
088    return result;
089  }
090
091  /**
092   * Creates a builder for configuring the SnakeToCamelCaseRule.
093   *
094   * @return new builder instance
095   */
096  public static Builder builder() {
097    return new Builder();
098  }
099
100  /**
101   * Creates a SnakeToCamelCaseRule with default settings (camelCase, not PascalCase).
102   *
103   * @return new rule instance with default configuration
104   */
105  public static SnakeToCamelCaseRule create() {
106    return new Builder().build();
107  }
108
109  /**
110   * Creates a SnakeToCamelCaseRule that produces PascalCase output.
111   *
112   * @return new rule instance configured for PascalCase
113   */
114  public static SnakeToCamelCaseRule createPascalCase() {
115    return new Builder().capitalizeFirst(true).build();
116  }
117
118  /** Builder class for SnakeToCamelCaseRule configuration. */
119  public static class Builder {
120    private boolean capitalizeFirst = false;
121    private boolean preserveUnderscores = false;
122
123    /**
124     * Sets whether to capitalize the first letter (PascalCase instead of camelCase).
125     *
126     * @param capitalize true for PascalCase, false for camelCase
127     * @return this builder
128     */
129    public Builder capitalizeFirst(boolean capitalize) {
130      this.capitalizeFirst = capitalize;
131      return this;
132    }
133
134    /**
135     * Sets whether to preserve existing underscores in the input.
136     *
137     * @param preserve true to preserve underscores, false to clean them up
138     * @return this builder
139     */
140    public Builder preserveUnderscores(boolean preserve) {
141      this.preserveUnderscores = preserve;
142      return this;
143    }
144
145    /**
146     * Builds the SnakeToCamelCaseRule with current configuration.
147     *
148     * @return configured rule instance
149     */
150    public SnakeToCamelCaseRule build() {
151      return new SnakeToCamelCaseRule(capitalizeFirst, preserveUnderscores);
152    }
153  }
154
155  @Override
156  public String toString() {
157    return String.format(
158        "SnakeToCamelCaseRule{capitalizeFirst=%s, preserveUnderscores=%s}",
159        capitalizeFirst, preserveUnderscores);
160  }
161}