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}