diff --git a/lib/src/main/java/io/cloudquery/caser/Caser.java b/lib/src/main/java/io/cloudquery/caser/Caser.java new file mode 100644 index 0000000..937f1f8 --- /dev/null +++ b/lib/src/main/java/io/cloudquery/caser/Caser.java @@ -0,0 +1,208 @@ +package io.cloudquery.caser; + +import lombok.Builder; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static io.cloudquery.caser.Initialisms.*; +import static io.cloudquery.caser.Initialisms.COMMON_INITIALISMS; + +@Builder +public class Caser { + @Builder.Default + private Set initialisms = new HashSet<>(COMMON_INITIALISMS); + + @Builder.Default + private Map snakeToCamelExceptions = new HashMap<>(); + + @Builder.Default + private Map camelToSnakeExceptions = new HashMap<>(); + + @Builder.Default + private Map customExceptions = new HashMap<>(); + + @Builder.Default + private Set customInitialisms = new HashSet<>(); + + public Caser(Set initialisms, + Map snakeToCamelExceptions, + Map camelToSnakeExceptions, + Map customExceptions, + Set customInitialisms) { + this.initialisms = initialisms; + this.snakeToCamelExceptions = snakeToCamelExceptions; + this.camelToSnakeExceptions = camelToSnakeExceptions; + this.customExceptions = customExceptions; + this.customInitialisms = customInitialisms; + + + HashMap combinedExceptions = new HashMap<>(COMMON_EXCEPTIONS); + combinedExceptions.putAll(customExceptions); + for (String key : combinedExceptions.keySet()) { + snakeToCamelExceptions.put(key, combinedExceptions.get(key)); + camelToSnakeExceptions.put(combinedExceptions.get(key), key); + } + + initialisms.addAll(customInitialisms); + } + + public String toSnake(String s) { + List words = new ArrayList<>(); + int lastPos = 0; + for (int i = 0; i < s.length(); i++) { + if (i > 0 && Character.isUpperCase(s.charAt(i))) { + + String initialism = startsWithInitialism(s.substring(lastPos)); + if (!initialism.isEmpty()) { + words.add(initialism); + i = lastPos + initialism.length(); + lastPos = i; + continue; + } + + String capWord = getCapWord(s.substring(lastPos)); + if (!capWord.isEmpty()) { + words.add(capWord); + i = lastPos + capWord.length(); + lastPos = i; + continue; + } + + words.add(s.substring(lastPos, i)); + lastPos = i; + } + } + + if (!s.substring(lastPos).isEmpty()) { + String w = s.substring(lastPos); + if (w.equals("s")) { + String lastWord = words.remove(words.size() - 1); + words.add(lastWord + w); + } else { + words.add(s.substring(lastPos)); + } + } + + return String.join("_", words).toLowerCase(); + } + + /** + * Returns a string converted from snake case to camel case. + *

+ * + * @param s The input string + * @return The string converted to camel case + */ + public String toCamel(String s) { + if (s.isEmpty()) { + return s; + } + List words = Arrays.asList(s.split("_")); + return String.join("", capitalize(words)); + } + + /** + * Returns a string converted from snake case to title case. + *

+ * Title case is similar to camel case, but spaces are used in between words. + * + * @param s The input string + * @return The string converted to title case + */ + public String toTitle(String s) { + if (s.isEmpty()) { + return s; + } + String[] words = s.split("_"); + if (!snakeToCamelExceptions.containsKey(words[0].toLowerCase())) { + words[0] = words[0].substring(0, 1).toUpperCase() + words[0].substring(1).toLowerCase(); + } + return String.join(" ", capitalize(Arrays.asList(words))); + } + + /** + * Returns a string converted from snake case to pascal case + * + * @param s The input string + * @return The string converted to pascal case + */ + public String toPascal(String s) { + if (s.isEmpty()) { + return s; + } + String camel = toCamel(s); + return camel.substring(0, 1).toUpperCase() + camel.substring(1); + } + + /** + * gets the next sequence of capitalized letters as a single word. + *

+ * If there is a word after capitalized sequence it leaves one letter as beginning of the next word + * + * @param s The input string + * @return A single word + */ + private String getCapWord(String s) { + for (int i = 0; i < s.length(); i++) { + if (!Character.isUpperCase(s.charAt(i))) { + if (i == 0) { + return ""; + } + return s.substring(0, i - 1); + } + } + return s; + } + + /** + * Returns the initialism if the given string begins with it + * + * @param s The input string + * @return The initialism if the given string begins with it, otherwise an empty string + */ + private String startsWithInitialism(String s) { + String initialism = ""; + + // the longest initialism is 5 char, the shortest 2 we choose the longest match + for (int i = 1; i <= s.length() && i <= 5; i++) { + if (s.length() > i - 1 && this.initialisms.contains(s.substring(0, i)) && s.substring(0, i).length() > initialism.length()) { + initialism = s.substring(0, i); + } + } + + return initialism; + } + + private List capitalize(List words) { + int n = words.stream().map(String::length).reduce(0, Integer::sum); + + List results = new ArrayList<>(); + for (int i = 0; i < words.size(); i++) { + if (snakeToCamelExceptions.containsKey(words.get(i))) { + results.add(snakeToCamelExceptions.get(words.get(i))); + continue; + } + + if (i > 0) { + String upper = words.get(i).toUpperCase(); + if (n > i - 1 && initialisms.contains(upper)) { + results.add(upper); + continue; + } + } + + if (i > 0 && !words.get(i).isEmpty()) { + results.add(words.get(i).substring(0, 1).toUpperCase()+words.get(i).substring(1)); + } else { + results.add(words.get(i)); + } + } + return results; + } +} \ No newline at end of file diff --git a/lib/src/main/java/io/cloudquery/caser/Initialisms.java b/lib/src/main/java/io/cloudquery/caser/Initialisms.java new file mode 100644 index 0000000..ffe498e --- /dev/null +++ b/lib/src/main/java/io/cloudquery/caser/Initialisms.java @@ -0,0 +1,63 @@ +package io.cloudquery.caser; + +import java.util.Map; +import java.util.Set; + +public class Initialisms { + public static final Set COMMON_INITIALISMS = Set.of( + "ACL", + "API", + "ASCII", + "CIDR", + "CPU", + "CSS", + "DNS", + "EOF", + "FQDN", + "GUID", + "HTML", + "HTTP", + "HTTPS", + "ID", + "IP", + "IPC", + "IPv4", + "IPv6", + "JSON", + "LHS", + "PID", + "QOS", + "QPS", + "RAM", + "RHS", + "RPC", + "SLA", + "SMTP", + "SQL", + "SSH", + "TCP", + "TLS", + "TTL", + "UDP", + "UI", + "UID", + "UUID", + "URI", + "URL", + "UTF8", + "VM", + "XML", + "XMPP", + "XSRF", + "XSS" + ); + + public static final Map COMMON_EXCEPTIONS = Map.of( + "oauth", "OAuth", + "ipv4", "IPv4", + "ipv6", "IPv6" + ); + + private Initialisms() { + } +} diff --git a/lib/src/test/java/io/cloudquery/caser/CaserTest.java b/lib/src/test/java/io/cloudquery/caser/CaserTest.java new file mode 100644 index 0000000..f4144f8 --- /dev/null +++ b/lib/src/test/java/io/cloudquery/caser/CaserTest.java @@ -0,0 +1,161 @@ +package io.cloudquery.caser; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +public class CaserTest { + public static Stream toSnakeSource() { + return Stream.of( + Arguments.of("TestCamelCase", "test_camel_case"), + Arguments.of("TestCamelCase", "test_camel_case"), + Arguments.of("AccountID", "account_id"), + Arguments.of("IDs", "ids"), + Arguments.of("PostgreSQL", "postgre_sql"), + Arguments.of("QueryStoreRetention", "query_store_retention"), + Arguments.of("TestCamelCaseLongString", "test_camel_case_long_string"), + Arguments.of("testCamelCaseLongString", "test_camel_case_long_string"), + Arguments.of("testIPv4", "test_ipv4"), + Arguments.of("CoreIPs", "core_ips"), + Arguments.of("CoreIps", "core_ips"), + Arguments.of("CoreV1", "core_v1"), + Arguments.of("APIVersion", "api_version"), + Arguments.of("TTLSecondsAfterFinished", "ttl_seconds_after_finished"), + Arguments.of("PodCIDRs", "pod_cidrs"), + Arguments.of("IAMRoles", "iam_roles"), + Arguments.of("testIAM", "test_iam"), + Arguments.of("TestAWSMode", "test_aws_mode") + ); + } + + public static Stream toCamelSource() { + return Stream.of( + Arguments.of("testCamelCase", "test_camel_case"), + Arguments.of("accountID", "account_id"), + Arguments.of("arns", "arns"), + Arguments.of("postgreSQL", "postgre_sql"), + Arguments.of("queryStoreRetention", "query_store_retention"), + Arguments.of("testCamelCaseLongString", "test_camel_case_long_string"), + Arguments.of("testCamelCaseLongString", "test_camel_case_long_string"), + Arguments.of("testIPv4", "test_ipv4") + ); + } + + public static Stream toTitleSource() { + return Stream.of( + Arguments.of("Test Camel Case", "test_camel_case"), + Arguments.of("Account ID", "account_id"), + Arguments.of("ARNs", "arns"), + Arguments.of("Postgre SQL", "postgre_sql"), + Arguments.of("Query Store Retention", "query_store_retention"), + Arguments.of("Test Camel Case Long String", "test_camel_case_long_string"), + Arguments.of("Test Camel Case Long String", "test_camel_case_long_string"), + Arguments.of("Test IPv4", "test_ipv4"), + Arguments.of("AWS Test Table", "aws_test_table"), + Arguments.of("Gcp Test Table", "gcp_test_table") + ); + } + + public static Stream toPascalSource() { + return Stream.of( + Arguments.of("TestCamelCase", "test_camel_case"), + Arguments.of("AccountID", "account_id"), + Arguments.of("Arns", "arns"), + Arguments.of("PostgreSQL", "postgre_sql"), + Arguments.of("QueryStoreRetention", "query_store_retention"), + Arguments.of("TestCamelCaseLongString", "test_camel_case_long_string"), + Arguments.of("TestCamelCaseLongString", "test_camel_case_long_string"), + Arguments.of("TestV1", "test_v1"), + Arguments.of("TestIPv4", "test_ipv4"), + Arguments.of("Ec2", "ec2"), + Arguments.of("S3", "s3") + ); + } + + public static Stream inversionSource() { + return Stream.of( + Arguments.of("TestCamelCase"), + Arguments.of("AccountID"), + Arguments.of("Arns"), + Arguments.of("PostgreSQL"), + Arguments.of("QueryStoreRetention"), + Arguments.of("TestCamelCaseLongString"), + Arguments.of("TestCamelCaseLongString"), + Arguments.of("TestV1"), + Arguments.of("TestIPv4"), + Arguments.of("TestIPv4"), + Arguments.of("S3") + ); + } + + public static Stream configureSource() { + return Stream.of( + Arguments.of("CDNs", "cdns"), + Arguments.of("ARNs", "arns"), + Arguments.of("EC2", "ec2"), + Arguments.of("S3", "s3") + ); + } + + public static Stream customExceptionsSource() { + return Stream.of( + Arguments.of("TEst", "test"), + Arguments.of("TTv2", "ttv2") + ); + } + + @ParameterizedTest + @MethodSource("toSnakeSource") + public void testToSnake(String camel, String snake) { + Assertions.assertEquals(snake, Caser.builder().build().toSnake(camel)); + } + + @ParameterizedTest + @MethodSource("toCamelSource") + public void testToCamel(String camel, String snake) { + Assertions.assertEquals(camel, Caser.builder().build().toCamel(snake)); + } + + @ParameterizedTest + @MethodSource("toTitleSource") + public void testToTitle(String title, String snake) { + Caser caser = Caser.builder().customExceptions(Map.of( + "arns", "ARNs", + "aws", "AWS" + )).build(); + Assertions.assertEquals(title, caser.toTitle(snake)); + } + + @ParameterizedTest + @MethodSource("toPascalSource") + public void testToPascal(String pascal, String snake) { + Caser caser = Caser.builder().build(); + Assertions.assertEquals(pascal, caser.toPascal(snake)); + } + + @ParameterizedTest + @MethodSource("inversionSource") + public void testInversion(String pascal) { + Caser caser = Caser.builder().build(); + Assertions.assertEquals(pascal, caser.toPascal(caser.toSnake(pascal))); + } + + @ParameterizedTest + @MethodSource("configureSource") + public void testConfigure(String camel, String snake) { + Caser caser = Caser.builder().customInitialisms(Set.of("CDN", "ARN", "EC2")).build(); + Assertions.assertEquals(snake, caser.toSnake(camel)); + } + + @ParameterizedTest + @MethodSource("customExceptionsSource") + public void testCustomExceptions(String camel, String snake) { + Caser caser = Caser.builder().customExceptions(Map.of("test", "TEst", "ttv2", "TTv2")).build(); + Assertions.assertEquals(camel, caser.toCamel(snake)); + } +} \ No newline at end of file