Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lint: add kebab-case checks #274

Merged
merged 11 commits into from
Apr 10, 2021
19 changes: 10 additions & 9 deletions src/lint/track_config.nim
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,14 @@ const
proc isValidConceptExercise(data: JsonNode; context: string; path: Path): bool =
if isObject(data, context, path):
let checks = [
hasString(data, "slug", path, context),
hasString(data, "slug", path, context, checkIsKebab = true),
hasString(data, "name", path, context),
hasString(data, "uuid", path, context),
hasBoolean(data, "deprecated", path, context, isRequired = false),
hasArrayOfStrings(data, "concepts", path, context,
allowedArrayLen = 0..int.high),
allowedArrayLen = 0..int.high, checkIsKebab = true),
hasArrayOfStrings(data, "prerequisites", path, context,
allowedArrayLen = 0..int.high),
allowedArrayLen = 0..int.high, checkIsKebab = true),
hasString(data, "status", path, context, isRequired = false,
allowed = statuses),
]
Expand All @@ -91,15 +91,15 @@ proc isValidPracticeExercise(data: JsonNode; context: string;
path: Path): bool =
if isObject(data, context, path):
let checks = [
hasString(data, "slug", path, context),
hasString(data, "slug", path, context, checkIsKebab = true),
hasString(data, "name", path, context),
hasString(data, "uuid", path, context),
hasBoolean(data, "deprecated", path, context, isRequired = false),
hasInteger(data, "difficulty", path, context, allowed = 0..10),
hasArrayOfStrings(data, "practices", path, context,
allowedArrayLen = 0..int.high),
allowedArrayLen = 0..int.high, checkIsKebab = true),
hasArrayOfStrings(data, "prerequisites", path, context,
allowedArrayLen = 0..int.high),
allowedArrayLen = 0..int.high, checkIsKebab = true),
hasString(data, "status", path, context, isRequired = false,
allowed = statuses),
]
Expand All @@ -114,15 +114,16 @@ proc hasValidExercises(data: JsonNode; path: Path): bool =
allowedLength = 0..int.high),
hasArrayOf(exercises, "practice", path, isValidPracticeExercise, k,
allowedLength = 0..int.high),
hasArrayOfStrings(exercises, "foregone", path, k, isRequired = false),
hasArrayOfStrings(exercises, "foregone", path, k, isRequired = false,
checkIsKebab = true),
]
result = allTrue(checks)

proc isValidConcept(data: JsonNode; context: string; path: Path): bool =
if isObject(data, context, path):
let checks = [
hasString(data, "uuid", path, context),
hasString(data, "slug", path, context),
hasString(data, "slug", path, context, checkIsKebab = true),
hasString(data, "name", path, context),
]
result = allTrue(checks)
Expand Down Expand Up @@ -153,7 +154,7 @@ proc isValidTrackConfig(data: JsonNode; path: Path): bool =
if isObject(data, "", path):
let checks = [
hasString(data, "language", path),
hasString(data, "slug", path),
hasString(data, "slug", path, checkIsKebab = true),
hasBoolean(data, "active", path),
hasString(data, "blurb", path, maxLen = 400),
hasInteger(data, "version", path, allowed = 3..3),
Expand Down
34 changes: 27 additions & 7 deletions src/lint/validators.nim
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,16 @@ proc isUrlLike(s: string): bool =
const
emptySetOfStrings = initHashSet[string](0)

func isLowerKebab(s: string): bool =
## Returns true if `s` is a lowercase and kebab-case string.
result = true
for c in s:
if c notin {'a'..'z', '0'..'9', '-'}:
return false

proc isString*(data: JsonNode; key: string; path: Path; context: string;
isRequired = true; allowed = emptySetOfStrings;
checkIsUrlLike = false; maxLen = int.high;
checkIsUrlLike = false; maxLen = int.high; checkIsKebab = false;
isInArray = false): bool =
result = true
case data.kind
Expand All @@ -101,6 +108,16 @@ proc isString*(data: JsonNode; key: string; path: Path; context: string;
result.setFalseAndPrint(&"Not a valid URL: {q s}", path)
elif s.len > 0:
if not isEmptyOrWhitespace(s):
if checkIsKebab:
if not isLowerKebab(s):
let msg =
if isInArray:
&"The {format(context, key)} array contains {s}, but every " &
"value must be lowercase and kebab-case"
else:
&"The {format(context, key)} value is {s}, but it must be a " &
"lowercase and kebab-case string"
result.setFalseAndPrint(msg, path)
if not hasValidRuneLength(s, key, path, context, maxLen):
result = false
else:
Expand Down Expand Up @@ -131,10 +148,11 @@ proc isString*(data: JsonNode; key: string; path: Path; context: string;

proc hasString*(data: JsonNode; key: string; path: Path; context = "";
isRequired = true; allowed = emptySetOfStrings;
checkIsUrlLike = false; maxLen = int.high): bool =
checkIsUrlLike = false; maxLen = int.high;
checkIsKebab = false): bool =
if data.hasKey(key, path, context, isRequired):
result = isString(data[key], key, path, context, isRequired, allowed,
checkIsUrlLike, maxLen)
checkIsUrlLike, maxLen, checkIsKebab = checkIsKebab)
elif not isRequired:
result = true

Expand All @@ -143,7 +161,8 @@ proc isArrayOfStrings*(data: JsonNode;
path: Path;
isRequired = true;
allowed: HashSet[string];
allowedArrayLen: Slice): bool =
allowedArrayLen: Slice;
checkIsKebab: bool): bool =
## Returns true in any of these cases:
## - `data` is a `JArray` with length in `allowedArrayLen` that contains only
## non-empty, non-blank strings.
Expand All @@ -157,7 +176,7 @@ proc isArrayOfStrings*(data: JsonNode;
if arrayLen in allowedArrayLen:
for item in data:
if not isString(item, context, path, "", isRequired, allowed,
isInArray = true):
checkIsKebab = checkIsKebab, isInArray = true):
result = false
else:
let msgStart = &"The {q context} array has length {arrayLen}, " &
Expand All @@ -184,14 +203,15 @@ proc hasArrayOfStrings*(data: JsonNode;
context = "";
isRequired = true;
allowed = emptySetOfStrings;
allowedArrayLen = 1..int.high): bool =
allowedArrayLen = 1..int.high;
checkIsKebab = false): bool =
## Returns true in any of these cases:
## - `isArrayOfStrings` returns true for `data[key]`.
## - `data` lacks the key `key` and `isRequired` is false.
if data.hasKey(key, path, context, isRequired):
let contextAndKey = joinWithDot(context, key)
result = isArrayOfStrings(data[key], contextAndKey, path, isRequired,
allowed, allowedArrayLen)
allowed, allowedArrayLen, checkIsKebab = checkIsKebab)
elif not isRequired:
result = true

Expand Down