Skip to content

Commit

Permalink
Merge pull request #138 from wolfadex/valid-names
Browse files Browse the repository at this point in the history
Add tests to make sure toValueName/toTypeName actually produce valid names
  • Loading branch information
wolfadex authored Aug 9, 2024
2 parents ba46af5 + aded7a1 commit b36cbab
Show file tree
Hide file tree
Showing 3 changed files with 264 additions and 26 deletions.
6 changes: 3 additions & 3 deletions elm.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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": {}
}
Expand Down
117 changes: 96 additions & 21 deletions src/Common.elm
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ module Common exposing
, unwrapUnsafe
)

import Regex
import String.Extra


Expand Down Expand Up @@ -84,19 +85,87 @@ toTypeName (UnsafeName name) =
|> String.replace "_" " "
|> String.trim
|> String.Extra.toTitleCase
|> deSymbolify " "
|> String.replace " " ""
|> 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.
Expand All @@ -119,7 +188,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__"
Expand All @@ -140,30 +209,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
Expand Down
167 changes: 165 additions & 2 deletions tests/TestCommon.elm
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -21,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"
]


Expand All @@ -42,9 +47,167 @@ toTypeName =
, toTypeNameTest "not-found" "NotFound"
, toTypeNameTest "not_found" "NotFound"
, toTypeNameTest "PAS (2013)" "PAS2013"
, toTypeNameTest "PAS2035 [2019]" "PAS2035_2019"
]


toTypeNameIdempotence : Test.Test
toTypeNameIdempotence =
Test.fuzz inputFuzzer "toTypeName is idempotent" <|
\input ->
let
typed : String
typed =
input
|> Common.UnsafeName
|> Common.toTypeName
in
if typed == "Empty__" || typed == "Dollar__" then
Expect.pass

else
typed
|> Common.UnsafeName
|> Common.toTypeName
|> 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" <|
\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) <|
Expand Down

0 comments on commit b36cbab

Please sign in to comment.