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: begin linting concept exercise config.json #169

Merged
merged 8 commits into from
Feb 5, 2021
6 changes: 6 additions & 0 deletions src/helpers.nim
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,9 @@ proc getSortedSubdirs*(dir: string): seq[string] =
if kind == pcDir:
result.add path
sort result

template writeError*(description: string, details: string) =
stdout.styledWriteLine(fgRed, description & ":")
stdout.writeLine(details)
stdout.write "\n"
result = false
42 changes: 42 additions & 0 deletions src/lint/concept_exercises.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import std/[json, os, terminal]
import ".."/helpers
import "."/validators

proc isValidAuthorOrContributor(data: JsonNode, context: string, path: string): bool =
if isObject(data, context, path):
result = true
checkString("github_username")
checkString("exercism_username", isRequired = false)

template checkFiles(data: JsonNode, context, path: string) =
if hasObject(data, context, path):
checkArrayOfStrings(context, "solution")
checkArrayOfStrings(context, "test")
checkArrayOfStrings(context, "exemplar")
else:
result = false

proc isValidConceptExerciseConfig(data: JsonNode, path: string): bool =
if isObject(data, "root", path):
result = true
checkArrayOf("authors", isValidAuthorOrContributor)
checkArrayOf("contributors", isValidAuthorOrContributor, isRequired = false)
checkFiles(data, "files", path)
checkArrayOfStrings("", "forked_from", isRequired = false)
checkString("language_versions", isRequired = false)

proc isEveryConceptExerciseConfigValid*(trackDir: string): bool =
let conceptExercisesDir = trackDir / "exercises" / "concept"
result = true
if dirExists(conceptExercisesDir):
for exerciseDir in getSortedSubdirs(conceptExercisesDir):
let configPath = exerciseDir / ".meta" / "config.json"
if fileExists(configPath):
let j =
try:
parseFile(configPath)
except:
writeError("JSON parsing error", getCurrentExceptionMsg())
continue
if not isValidConceptExerciseConfig(j, configPath):
result = false
13 changes: 5 additions & 8 deletions src/lint/lint.nim
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import std/[json, os, terminal]
import ".."/[cli, helpers]

template writeError(description: string, details: string) =
stdout.styledWriteLine(fgRed, description & ":")
stdout.writeLine(details)
stdout.write "\n"
result = false
import "."/concept_exercises

proc isValidTrackConfig(trackDir: string): bool =
result = true
Expand Down Expand Up @@ -68,12 +63,14 @@ proc lint*(conf: Conf) =
let b1 = isValidTrackConfig(trackDir)
let b2 = conceptExerciseFilesExist(trackDir)
let b3 = conceptFilesExist(trackDir)
let b4 = isEveryConceptExerciseConfigValid(trackDir)

if b1 and b2 and b3:
if b1 and b2 and b3 and b4:
echo """
Basic linting finished successfully:
- config.json exists and is valid JSON
- Every concept has the required .md files and links.json file
- Every concept exercise has the required .md files and a .meta/config.json file
- Every concept has the required .md files and links.json file"""
- Every concept exercise .meta/config.json file is valid"""
else:
quit(1)
78 changes: 78 additions & 0 deletions src/lint/validators.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import std/[json, strutils, terminal]
import ".."/helpers
export strutils.strip

proc q(s: string): string =
"'" & s & "'"

proc isObject*(data: JsonNode; context, path: string): bool =
result = true
if data.kind != JObject:
writeError("Not an object: " & q(context), path)

proc hasObject*(data: JsonNode; key, path: string,
isRequired = true): bool =
result = true
if data.hasKey(key):
if data[key].kind != JObject:
writeError("Not an object: " & q(key), path)
elif isRequired:
writeError("Missing key: " & q(key), path)

template checkString*(key: string, isRequired = true) =
if data.hasKey(key):
if data[key].kind == JString:
let s = data[key].getStr()
if s.len == 0:
writeError("String is zero-length: " & q(key), path)
elif s.strip().len == 0:
writeError("String is whitespace-only: " & q(key), path)
else:
writeError("Not a string: " & q(key) & ": " & $data[key], path)
elif isRequired:
writeError("Missing key: " & q(key), path)

proc format(context, key: string): string =
if context.len > 0:
q(context & "." & key)
else:
q(key)

template checkArrayOfStrings*(context, key: string; isRequired = true) =
var d = if context.len == 0: data else: data[context]
if d.hasKey(key):
if d[key].kind == JArray:
if d[key].len == 0:
if isRequired:
writeError("Array is empty: " & format(context, key), path)
else:
for item in d[key]:
if item.kind == JString:
let s = item.getStr()
if s.len == 0:
writeError("Array contains zero-length string: " & format(context, key), path)
elif s.strip().len == 0:
writeError("Array contains whitespace-only string: " & q(key), path)
else:
writeError("Array contains non-string: " & format(context, key) & ": " & $item, path)
else:
writeError("Not an array: " & format(context, key), path)
elif isRequired:
writeError("Missing key: " & format(context, key), path)

template checkArrayOf*(key: string,
call: proc(d: JsonNode; key, path: string): bool,
isRequired = true) =
if data.hasKey(key):
if data[key].kind == JArray:
if data[key].len == 0:
if isRequired:
writeError("Array is empty: " & q(key), path)
else:
for item in data[key]:
if not call(item, key, path):
result = false
else:
writeError("Not an array: " & q(key), path)
elif isRequired:
writeError("Missing key: " & q(key), path)