From d4623e0f9dd18d4c3f9fa3faade8d56c70147394 Mon Sep 17 00:00:00 2001 From: Leonardo Taglialegne Date: Wed, 7 Aug 2024 21:10:36 +0200 Subject: [PATCH 1/3] Add tests to make sure toValueName/toTypeName actually produce valid names --- elm.json | 4 +- src/Common.elm | 40 +++++++------ tests/TestCommon.elm | 139 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 163 insertions(+), 20 deletions(-) diff --git a/elm.json b/elm.json index 2f7e08c..01bd5e0 100644 --- a/elm.json +++ b/elm.json @@ -50,7 +50,6 @@ "jluckyiv/elm-utc-date-strings": "1.0.0", "justinmimbs/date": "4.1.0", "miniBill/elm-codec": "2.1.0", - "miniBill/elm-unicode": "1.1.1", "myrho/elm-parser-extras": "1.0.1", "noahzgordon/elm-color-extra": "1.0.2", "robinheghan/fnv1a": "1.0.0", @@ -68,7 +67,8 @@ }, "test-dependencies": { "direct": { - "elm-explorations/test": "2.2.0" + "elm-explorations/test": "2.2.0", + "miniBill/elm-unicode": "1.1.1" }, "indirect": {} } diff --git a/src/Common.elm b/src/Common.elm index 9baae33..6592af0 100644 --- a/src/Common.elm +++ b/src/Common.elm @@ -84,7 +84,7 @@ toTypeName (UnsafeName name) = |> String.replace "_" " " |> String.trim |> String.Extra.toTitleCase - |> deSymbolify " " + |> deSymbolify ' ' |> String.replace " " "" |> String.Extra.toTitleCase @@ -95,7 +95,7 @@ toValueName : UnsafeName -> String toValueName (UnsafeName name) = name |> String.replace " " "_" - |> deSymbolify "_" + |> deSymbolify '_' |> initialUppercaseWordToLowercase @@ -119,7 +119,7 @@ nameFromStatusCode name = {-| Sometimes a word in the schema contains invalid characers for an Elm name. We don't want to completely remove them though. -} -deSymbolify : String -> String -> String +deSymbolify : Char -> String -> String deSymbolify replacement str = if str == "$" then "dollar__" @@ -140,30 +140,36 @@ deSymbolify replacement str = let removeLeadingUnderscores : String -> String removeLeadingUnderscores acc = - if String.isEmpty acc then - "empty__" + case String.uncons acc of + Nothing -> + "empty__" - else if String.startsWith "_" acc then - removeLeadingUnderscores (String.dropLeft 1 acc) + Just ( head, tail ) -> + if head == replacement then + removeLeadingUnderscores tail - else - acc + else if Char.isDigit head then + "N" ++ acc + + else + acc in str |> replaceSymbolsWith replacement |> removeLeadingUnderscores -replaceSymbolsWith : String -> String -> String +replaceSymbolsWith : Char -> String -> String replaceSymbolsWith replacement input = input - |> String.replace "-" replacement - |> String.replace "+" replacement - |> String.replace "$" replacement - |> String.replace "(" replacement - |> String.replace ")" replacement - |> String.replace "/" replacement - |> String.replace "," replacement + |> String.map + (\c -> + if c /= '_' && not (Char.isAlphaNum c) then + replacement + + else + c + ) initialUppercaseWordToLowercase : String -> String diff --git a/tests/TestCommon.elm b/tests/TestCommon.elm index f784654..f3882de 100644 --- a/tests/TestCommon.elm +++ b/tests/TestCommon.elm @@ -1,8 +1,12 @@ -module TestCommon exposing (toTypeName, toValueName) +module TestCommon exposing (toTypeName, toTypeNameIdempotence, toTypeNameSafety, toValueName, toValueNameIdempotence, toValueNameSafety) import Common import Expect +import Fuzz +import Json.Encode +import Set exposing (Set) import Test +import Unicode toValueName : Test.Test @@ -45,6 +49,139 @@ toTypeName = ] +toTypeNameIdempotence : Test.Test +toTypeNameIdempotence = + Test.fuzz Fuzz.string "toTypeName is idempotent" <| + \input -> + let + typed : String + typed = + input + |> Common.UnsafeName + |> Common.toTypeName + in + if typed == "Empty__" then + Expect.pass + + else + typed + |> Common.UnsafeName + |> Common.toTypeName + |> Expect.equal typed + + +toTypeNameSafety : Test.Test +toTypeNameSafety = + Test.fuzz Fuzz.string "toTypeName produces a valid identifier" <| + \input -> + let + typed : String + typed = + input + |> Common.UnsafeName + |> Common.toTypeName + in + if Set.member typed reservedList then + Expect.fail "Invalid identifier: reserved word" + + else + case String.toList typed of + [] -> + Expect.fail "Invalid identifier: it is empty" + + head :: tail -> + if isUpper head && List.all isAlphaNumOrUnderscore tail then + Expect.pass + + else + Expect.fail ("Invalid type name " ++ escape typed) + + +toValueNameIdempotence : Test.Test +toValueNameIdempotence = + Test.fuzz Fuzz.string "toValueName is idempotent" <| + \input -> + let + typed : String + typed = + input + |> Common.UnsafeName + |> Common.toValueName + in + typed + |> Common.UnsafeName + |> Common.toValueName + |> Expect.equal typed + + +toValueNameSafety : Test.Test +toValueNameSafety = + Test.fuzz Fuzz.string "toValueName produces a valid identifier" <| + \input -> + let + typed : String + typed = + input + |> Common.UnsafeName + |> Common.toValueName + in + if Set.member typed reservedList then + Expect.fail "Invalid identifier: reserved word" + + else + case String.toList typed of + [] -> + Expect.fail "Invalid identifier: it is empty" + + head :: tail -> + if isLower head && List.all isAlphaNumOrUnderscore tail then + Expect.pass + + else + Expect.fail ("Invalid value name " ++ escape typed) + + +reservedList : Set String +reservedList = + -- Copied from elm-syntax + [ "module" + , "exposing" + , "import" + , "as" + , "if" + , "then" + , "else" + , "let" + , "in" + , "case" + , "of" + , "port" + , "type" + , "where" + ] + |> Set.fromList + + +isUpper : Char -> Bool +isUpper c = + Char.isUpper c || Unicode.isUpper c + + +isLower : Char -> Bool +isLower c = + Char.isLower c || Unicode.isLower c + + +isAlphaNumOrUnderscore : Char -> Bool +isAlphaNumOrUnderscore c = + Char.isAlphaNum c || c == '_' || Unicode.isAlphaNum c + + +escape : String -> String +escape input = + Json.Encode.encode 0 (Json.Encode.string input) + + toValueNameTest : String -> String -> Test.Test toValueNameTest from to = Test.test ("\"" ++ from ++ "\" becomes value name " ++ to) <| From 7d9d40153a2f76e1a96783d0a75723c5d146b841 Mon Sep 17 00:00:00 2001 From: Leonardo Taglialegne Date: Wed, 7 Aug 2024 21:40:27 +0200 Subject: [PATCH 2/3] Add more edge cases --- elm.json | 2 +- src/Common.elm | 80 +++++++++++++++++++++++++++++++++++++++++--- tests/TestCommon.elm | 32 ++++++++++++++++-- 3 files changed, 105 insertions(+), 9 deletions(-) diff --git a/elm.json b/elm.json index 01bd5e0..4b2a6af 100644 --- a/elm.json +++ b/elm.json @@ -11,6 +11,7 @@ "dillonkearns/elm-pages": "10.1.0", "elm/core": "1.0.5", "elm/json": "1.1.3", + "elm/regex": "1.0.0", "elm/url": "1.0.0", "elmcraft/core-extra": "2.0.0", "json-tools/json-schema": "1.0.2", @@ -39,7 +40,6 @@ "elm/http": "2.0.0", "elm/parser": "1.1.0", "elm/random": "1.0.0", - "elm/regex": "1.0.0", "elm/time": "1.0.0", "elm/virtual-dom": "1.0.3", "elm-community/basics-extra": "4.1.0", diff --git a/src/Common.elm b/src/Common.elm index 6592af0..31ea8bb 100644 --- a/src/Common.elm +++ b/src/Common.elm @@ -16,6 +16,7 @@ module Common exposing , unwrapUnsafe ) +import Regex import String.Extra @@ -85,18 +86,87 @@ toTypeName (UnsafeName name) = |> String.trim |> String.Extra.toTitleCase |> deSymbolify ' ' - |> String.replace " " "" + |> Debug.log "After deSymbolify" + |> reduceWith replaceSpacesRegex + (\match -> + case match.submatches of + [ Just before, Just after ] -> + case String.toInt before of + Nothing -> + before ++ after + + Just _ -> + case String.toInt after of + Nothing -> + before ++ after + + Just _ -> + match.match + + [ Just before, Nothing ] -> + before + + [ Nothing, Just after ] -> + after + + _ -> + "" + ) + |> String.replace " " "_" |> String.Extra.toTitleCase +replaceSpacesRegex : Regex.Regex +replaceSpacesRegex = + Regex.fromString "(.)? (.)?" + |> Maybe.withDefault Regex.never + + {-| Convert into a name suitable to be used in Elm as a variable. -} toValueName : UnsafeName -> String toValueName (UnsafeName name) = - name - |> String.replace " " "_" - |> deSymbolify '_' - |> initialUppercaseWordToLowercase + let + raw : String + raw = + name + |> String.replace " " "_" + |> deSymbolify '_' + in + if raw == "dollar__" || raw == "empty__" then + raw + + else + raw + |> reduceWith replaceUnderscoresRegex + (\{ match } -> + if match == "__" then + "_" + + else + "" + ) + |> initialUppercaseWordToLowercase + + +replaceUnderscoresRegex : Regex.Regex +replaceUnderscoresRegex = + Regex.fromString "(?:__)|(?:_$)" + |> Maybe.withDefault Regex.never + + +reduceWith : Regex.Regex -> (Regex.Match -> String) -> String -> String +reduceWith regex map input = + let + output : String + output = + Regex.replace regex map input + in + if output == input then + input + + else + reduceWith regex map output {-| Some OAS have response refs that are just the status code. diff --git a/tests/TestCommon.elm b/tests/TestCommon.elm index f3882de..4c02822 100644 --- a/tests/TestCommon.elm +++ b/tests/TestCommon.elm @@ -25,7 +25,8 @@ toValueName = , toValueNameTest "SHA256-DSA" "sha256_DSA" , toValueNameTest "decode-not-found" "decode_not_found" , toValueNameTest "not_found" "not_found" - , toValueNameTest "PAS (2013)" "pas__2013_" + , toValueNameTest "PAS (2013)" "pas_2013" + , toValueNameTest "PAS2035 [2019]" "pas2035_2019" ] @@ -46,12 +47,13 @@ toTypeName = , toTypeNameTest "not-found" "NotFound" , toTypeNameTest "not_found" "NotFound" , toTypeNameTest "PAS (2013)" "PAS2013" + , toTypeNameTest "PAS2035 [2019]" "PAS2035_2019" ] toTypeNameIdempotence : Test.Test toTypeNameIdempotence = - Test.fuzz Fuzz.string "toTypeName is idempotent" <| + Test.fuzz inputFuzzer "toTypeName is idempotent" <| \input -> let typed : String @@ -60,7 +62,7 @@ toTypeNameIdempotence = |> Common.UnsafeName |> Common.toTypeName in - if typed == "Empty__" then + if typed == "Empty__" || typed == "Dollar__" then Expect.pass else @@ -70,6 +72,30 @@ toTypeNameIdempotence = |> Expect.equal typed +inputFuzzer : Fuzz.Fuzzer String +inputFuzzer = + Fuzz.oneOf + [ Fuzz.string + , Fuzz.oneOfValues + [ "-1" + , "+1" + , "$" + , "$res" + , "" + , "$___" + , "X-API-KEY" + , "PASVersion" + , "MACOS" + , "SHA256" + , "SHA256-DSA" + , "decode-not-found" + , "not_found" + , "PAS (2013)" + , "PAS2035 [2019]" + ] + ] + + toTypeNameSafety : Test.Test toTypeNameSafety = Test.fuzz Fuzz.string "toTypeName produces a valid identifier" <| From aded7a19eccbb22374f9aabafb35d10cc3899b72 Mon Sep 17 00:00:00 2001 From: Leonardo Taglialegne Date: Wed, 7 Aug 2024 21:45:10 +0200 Subject: [PATCH 3/3] Remove stray Debug.log --- src/Common.elm | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Common.elm b/src/Common.elm index 31ea8bb..ad020a6 100644 --- a/src/Common.elm +++ b/src/Common.elm @@ -86,7 +86,6 @@ toTypeName (UnsafeName name) = |> String.trim |> String.Extra.toTitleCase |> deSymbolify ' ' - |> Debug.log "After deSymbolify" |> reduceWith replaceSpacesRegex (\match -> case match.submatches of