aboutsummaryrefslogtreecommitdiff
path: root/script/validate_json.rb
blob: cb370ff2cfd8e80477fcec539aba77221a195560 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
#!/usr/bin/env ruby

# Validates JSON files in the _data directory

require 'json'

module ExitCodes
    SUCCESS = 0
    PARSE_FAILED = 1               # JSON parse errors
    UNSORTED = 2                   # data keys are not in alphanumeric order
    MISSING_URL = 3                # Entry missing the required 'url' field
    MISSING_DIFFICULTY = 4         # Entry missing the required 'difficulty' field
    MISSING_DOMAINS = 5            # Entry missing the required 'domains' field
    MISSING_LANGUAGE_KEYS = 6      # Translation missing required keys
    MISSING_NAME = 7               # Entry missing the required 'name' field
    UNEXPECTED_DIFFICULTY = 8      # Unexpected value for 'difficulty' field
    UNEXPECTED_LANGUAGE = 9        # Unexpected language code for 'url_code' field
    UNEXPECTED_LANGUAGE_KEY = 10   # Unexpected language key for translation
    UNSUPPORTED_FIELD = 11         # Unsupported field for site entry
    UNEXPECTED_NOTES = 12          # Unexpected notes key for translation
    DUPLICATES = 13                # Duplicate entries
end

SupportedDifficulties = ["easy", "medium", "hard", "limited", "impossible"]
SupportedEntryKeys = ["difficulty", "domains", "email", "email_body", "email_subject", "meta", "name", "notes", "url"]
SupportedLanguageKeys = [
    "about",
    "contribute",
    "defaultnote_easy",
    "defaultnote_email",
    "difficulty",
    "difficulty_easy",
    "difficulty_hard",
    "difficulty_impossible",
    "difficulty_limited",
    "difficulty_medium",
    "extension_browser",
    "extensionguide",
    "extensionp1",
    "extensionp2",
    "extensionp3",
    "extensionp4",
    "extensionp5",
    "footercredits",
    "guide",
    "guideeasy",
    "guideexplanations",
    "guidehard",
    "guideimpossible",
    "guidelimited",
    "guidemedium",
    "hideinfo",
    "jgmd",
    "name",
    "noinfo",
    "noresults",
    "noresultshelp",
    "popular",
    "pullrequest",
    "reset",
    "search",
    "sendmail",
    "showinfo",
    "tagline",
    "title",
    "twitter",
    "whatisthis",
    "whatisthis1",
    "whatisthis2",
    "whatisthis3",
    "whatisthis4"
]

def get_supported_languages()
    return translation_files = Dir.children('_data/trans/').map { |f| f.delete_suffix('.json') }
end

SupportedLanguages = get_supported_languages()

def get_transformed_name(site_object)
    return site_object['name'].downcase.sub(/^the\s+/, '')
end

def validate_accepted_keys(key)
    key.keys.each do |entry_key|
        if entry_key.start_with?('url_') || entry_key.start_with?('notes_')
            # These have their own validation methods
            next
        end

        unless SupportedEntryKeys.include?(entry_key)
            STDERR.puts "Entry '#{key['name']}' has unsupported field: "\
                        "'#{entry_key}'.\n"\
                        "Use one of the supported fields:\n"\
                        "\t#{SupportedEntryKeys}"
            exit ExitCodes::UNSUPPORTED_FIELD
        end
    end
end

def error_on_missing_field(key, field, exit_code)
    unless key.key?(field)
        STDERR.puts "Entry '#{key['name']}' has no '#{field}' field"
        exit exit_code
    end
end

def validate_difficulty(key)
    difficulty = key['difficulty']
    unless SupportedDifficulties.include?(difficulty)
        STDERR.puts "Entry '#{key['name']}' has unexpected 'difficulty' field:"\
                    "'#{difficulty}'.\n"\
                    "Use one of the supported difficulty values:\n"\
                    "\t#{SupportedDifficulties}"
        exit ExitCodes::UNEXPECTED_DIFFICULTY
    end
end

def validate_localized_urls(key)
    key.keys.each do |entry_key|
        if entry_key.start_with?('url_') && !SupportedLanguages.any? { |lang| entry_key.eql?("url_#{lang}") }
            STDERR.puts "Entry '#{key['name']}' has unrecognized language code: "\
                        "'#{entry_key}'.\n"\
                        "Use one of the supported languages:\n"\
                        "\t#{SupportedLanguages}"
            exit ExitCodes::UNEXPECTED_LANGUAGE
        end
    end
end

def validate_localized_notes(key)
    key.keys.each do |entry_key|
        if entry_key.start_with?('notes_') && !SupportedLanguages.any? { |lang| entry_key.eql?("notes_#{lang}") }
            STDERR.puts "Entry '#{key['name']}' has unrecognized notes code: "\
                        "'#{entry_key}'.\n"\
                        "Use one of the supported languages:\n"\
                        "\t#{SupportedLanguages}"
            exit ExitCodes::UNEXPECTED_NOTES
        end
    end
end

def validate_website_entry(key, i)
    unless key.key?('name')
        STDERR.puts "Entry #{i} has no 'name' field"
        exit ExitCodes::MISSING_NAME
    end
    validate_accepted_keys(key)
    error_on_missing_field(key, 'url', ExitCodes::MISSING_URL)
    error_on_missing_field(key, 'difficulty', ExitCodes::MISSING_DIFFICULTY)
    error_on_missing_field(key, 'domains', ExitCodes::MISSING_DOMAINS)
    validate_difficulty(key)
    validate_localized_urls(key)
    validate_localized_notes(key)
end

def add_valid_language_key(keys_in_language_json, key, file)
    if SupportedLanguageKeys.include?(key)
        keys_in_language_json << key
    else
        STDERR.puts "Invalid key '#{key}' for file '#{file}'"
        exit ExitCodes::UNEXPECTED_LANGUAGE_KEY
    end
end

def validate_site_translation(is_sites_json, keys_in_language_json, file)
    unless is_sites_json
        unless keys_in_language_json == SupportedLanguageKeys
            STDERR.puts "Missing language keys in '#{file}': "\
                        "'#{SupportedLanguageKeys - keys_in_language_json}'"
            exit ExitCodes::MISSING_LANGUAGE_KEYS
        end
    end
end

json_files = Dir.glob('_data/**/*').select { |f| File.file?(f) }
json_files.each do |file|
    begin
        json = JSON.parse(File.read(file))
        is_sites_json = File.basename(file) =~ /sites.json/
        keys_in_language_json = []

        # check for duplicates
        if json.uniq.length != json.length
            STDERR.puts file + " contains duplicates"
            exit ExitCodes::DUPLICATES
        end

        # check for alphabetical ordering
        json.each_with_index do |(key, _), i|
            # sites.json is an array of objects; this would expand to:
            #   key = { ... }
            #   i = 0
            # hence, the key variable holds the actual value
            if is_sites_json
                validate_website_entry(key, i)
                name = get_transformed_name(key)
                prev_name = get_transformed_name(json[i - 1])
            else
                name = key
                prev_name = json.keys[i - 1]
                add_valid_language_key(keys_in_language_json, key, file)
            end
            if i > 0 && prev_name > name
                STDERR.puts 'Sorting error in ' + file
                STDERR.puts 'Keys must be in alphanumeric order. ' + \
                            prev_name + ' needs to come after ' + name
                exit ExitCodes::UNSORTED
            end
        end
        validate_site_translation(is_sites_json, keys_in_language_json, file)
    rescue JSON::ParserError => error
        STDERR.puts 'JSON parsing error encountered!'
        STDERR.puts error.backtrace.join("\n")
        exit ExitCodes::PARSE_FAILED
    end
end

exit ExitCodes::SUCCESS