Skip to content

Commit

Permalink
lint: add kebab-case checks
Browse files Browse the repository at this point in the history
This commit implements these checks for a track `config.json`:
- The `"slug"` value must be a non-empty, non-blank, lowercased string
  using kebab-case
- The `"exercises.concept[].slug"` value must be a non-empty, non-blank,
  lowercased string using kebab-case
- The `"exercises.concept[].concepts"` values must be non-empty,
  non-blank, lowercased strings using kebab-case
- The `"exercises.concept[].prerequisites"` values must be non-empty,
  non-blank, lowercased strings using kebab-case
- The `"exercises.practice[].slug"` value must be a non-empty,
  non-blank, lowercased string using kebab-case
- The `"exercises.practice[].practices"` values must be non-empty,
  non-blank, lowercased strings using kebab-case
- The `"exercises.practice[].prerequisites"` values must be non-empty,
  non-blank, lowercased strings using kebab-case
- The `"exercises.foregone"` values must be non-empty, non-blank,
  lowercased strings using kebab-case
- The `"concepts[].slug"` value must be a non-empty, non-blank,
  lowercased string using kebab-case
  • Loading branch information
ee7 committed Apr 9, 2021
1 parent 4a9a01b commit e898324
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 16 deletions.
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

0 comments on commit e898324

Please sign in to comment.