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
11 changes: 11 additions & 0 deletions src/helpers.nim
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,14 @@ 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

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

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

template checkFiles(data: JsonNode, context, path: string) =
if isObject(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, "", 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
else:
writeWarning("Directory does not exist", conceptExercisesDir)
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)
66 changes: 66 additions & 0 deletions src/lint/validators.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import std/[json, terminal]
import ".."/helpers

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

proc isObject*(data: JsonNode, key: string, path: string,
isRequired = true): bool =
result = true
if key.len == 0:
if data.kind != JObject:
writeError("JSON root is not an object", path)
elif data.hasKey(key):
if data[key].kind != JObject:
writeError("Not an object: " & q(key), path)
elif isRequired:
writeError("Missing key: " & q(key), path)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about allowing the key parameter to be empty. I did not understand what the empty string did in if isObject(data, "", path):. I came up with two alternatives:

  • Split this up into two functions
  • Make the context a parameter with a default value

I personally prefer the second option, as the checkArrayOfStrings template has the same property (and empty context being specified).


template checkString*(key: string, isRequired = true) =
if data.hasKey(key):
if data[key].kind == JString:
if data[key].getStr().len == 0:
writeError("String is zero-length: " & q(key), path)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that the spec did not indicate that non-empty strings also not be non-blank (as in: not consist of only white space). I've just updated the spec. Could you update this check to reflect that the string not be empty after trimming?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

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:
writeError("Array is empty: " & format(context, key), path)
else:
for item in d[key]:
if item.kind != JString:
writeError("Array contains non-string: " & format(context, key) & ": " & $item, path)
elif item.getStr().len == 0:
writeError("Array contains zero-length string: " & format(context, key), path)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment regarding non-blank strings.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

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:
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)