]> code.delx.au - gnu-emacs-elpa/blob - validate.el
Fix copyright
[gnu-emacs-elpa] / validate.el
1 ;;; validate.el --- Schema validation for Emacs-lisp -*- lexical-binding: t; -*-
2
3 ;; Copyright (C) 2016 Free Software Foundation, Inc.
4
5 ;; Author: Artur Malabarba <emacs@endlessparentheses.com>
6 ;; Keywords: lisp
7 ;; Package-Requires: ((emacs "24.1") (cl-lib "0.5"))
8 ;; Version: 0.2
9
10 ;;; Commentary:
11 ;;
12 ;; This library offers two functions that perform schema validation.
13 ;; Use this is your Elisp packages to provide very informative error
14 ;; messages when your users accidentally misconfigure a variable.
15 ;; For instance, if everything is fine, these do the same thing:
16 ;;
17 ;; 1. (validate-variable 'cider-known-endpoints)
18 ;; 2. cider-known-endpoints
19 ;;
20 ;; However, if the user has misconfigured this variable, option
21 ;; 1. will immediately give them an informative error message, while
22 ;; option 2. won't say anything and will lead to confusing errors down
23 ;; the line.
24 ;;
25 ;; The format and language of the schemas is the same one used in the
26 ;; `:type' property of a `defcustom'.
27 ;;
28 ;; See: (info "(elisp) Customization Types")
29 ;;
30 ;; Both functions throw a `user-error' if the value in question
31 ;; doesn't match the schema, and return the value itself if it
32 ;; matches. The function `validate-variable' verifies whether the value of a
33 ;; custom variable matches its custom-type, while `validate-value' checks an
34 ;; arbitrary value against an arbitrary schema.
35 ;;
36 ;; Missing features: `:inline', `plist', `coding-system', `color',
37 ;; `hook', `restricted-sexp'.
38
39 ;;; License:
40 ;;
41 ;; This file is part of GNU Emacs.
42 ;;
43 ;; GNU Emacs is free software: you can redistribute it and/or modify
44 ;; it under the terms of the GNU General Public License as published by
45 ;; the Free Software Foundation, either version 3 of the License, or
46 ;; (at your option) any later version.
47 ;;
48 ;; GNU Emacs is distributed in the hope that it will be useful,
49 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
50 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
51 ;; GNU General Public License for more details.
52 ;;
53 ;; You should have received a copy of the GNU General Public License
54 ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
55
56 ;;; Code:
57 (require 'cl-lib)
58 (require 'seq)
59 (require 'cus-edit)
60
61 (defun validate--check-list-contents (values schemas)
62 "Check that all VALUES match all SCHEMAS."
63 (if (not (= (length values) (length schemas)))
64 "wrong number of elements"
65 (seq-find #'identity (seq-mapn #'validate--check values schemas))))
66
67 (defun validate--check (value schema)
68 "Return nil if VALUE matches SCHEMA.
69 If they don't match, return an explanation."
70 (let ((args (cdr-safe schema))
71 (expected-type (or (car-safe schema) schema))
72 (props nil))
73 (while (and (keywordp (car args)) (cdr args))
74 (setq props `(,(pop args) ,(pop args) ,@props)))
75 (setq args (or (plist-get props :args)
76 args))
77 (let ((r
78 (cl-labels ((wtype ;wrong-type
79 (tt) (unless (funcall (intern (format "%sp" tt)) value)
80 (format "not a %s" tt))))
81 ;; TODO: hook (top-level only).
82 (cl-case expected-type
83 ((sexp other) nil)
84 (variable (cond ((wtype 'symbol))
85 ((not (boundp value)) "this symbol has no variable binding")))
86 ((integer number float string character symbol function boolean face)
87 (wtype expected-type))
88 (regexp (cond ((ignore-errors (string-match value "") t) nil)
89 ((wtype 'string))
90 (t "not a valid regexp")))
91 (repeat (cond
92 ((or (not args) (cdr args)) (error "`repeat' needs exactly one argument"))
93 ((wtype 'list))
94 (t (let ((subschema (car args)))
95 (seq-some (lambda (v) (validate--check v subschema)) value)))))
96 ((const function-item variable-item) (unless (eq value (car args))
97 "not the expected value"))
98 (file (cond ((wtype 'string))
99 ((file-exists-p value) nil)
100 ((plist-get props :must-match) "file does not exist")
101 ((not (file-writable-p value)) "file is not accessible")))
102 (directory (cond ((wtype 'string))
103 ((file-directory-p value) nil)
104 ((file-exists-p value) "path is not a directory")
105 ((not (file-writable-p value)) "directory is not accessible")))
106 (key-sequence (and (wtype 'string)
107 (wtype 'vector)))
108 ;; TODO: `coding-system', `color'
109 (coding-system (wtype 'symbol))
110 (color (wtype 'string))
111 (cons (or (wtype 'cons)
112 (validate--check (car value) (car args))
113 (validate--check (cdr value) (cadr args))))
114 ((list group) (or (wtype 'list)
115 (validate--check-list-contents value args)))
116 (vector (or (wtype 'vector)
117 (validate--check-list-contents value args)))
118 (alist (let ((value-type (plist-get props :value-type))
119 (key-type (plist-get props :key-type)))
120 (cond ((not value-type) (error "`alist' needs a :value-type"))
121 ((not key-type) (error "`alist' needs a :key-type"))
122 ((wtype 'list))
123 (t (validate--check value
124 `(repeat (cons ,key-type ,value-type)))))))
125 ;; TODO: `plist'
126 ((choice radio) (if (not (cdr args))
127 (error "`choice' needs at least one argument")
128 (let ((gather (mapcar (lambda (x) (validate--check value x)) args)))
129 (when (seq-every-p #'identity gather)
130 (concat "all of the options failed\n "
131 (mapconcat #'identity gather "\n "))))))
132 ;; TODO: `restricted-sexp'
133 (set (or (wtype 'list)
134 (let ((failed (list t)))
135 (dolist (schema args)
136 (let ((elem (seq-find (lambda (x) (not (validate--check x schema)))
137 value
138 failed)))
139 (unless (eq elem failed)
140 (setq value (remove elem value)))))
141 (when value
142 (concat "the following values don't match any of the options:\n "
143 (mapconcat (lambda (x) (format "%s" x)) value "\n "))))))))))
144 (when r
145 (let ((print-length 4)
146 (print-level 2))
147 (format "Looking for `%S' in `%S' failed because:\n%s"
148 schema value r))))))
149
150 \f
151 ;;; Exposed API
152 ;;;###autoload
153 (defun validate-value (value schema &optional noerror)
154 "Check that VALUE matches SCHEMA.
155 If it matches return VALUE, otherwise signal a `user-error'.
156
157 If NOERROR is non-nil, return t to indicate a match and nil to
158 indicate a failure."
159 (let ((report (validate--check value schema)))
160 (if report
161 (unless noerror
162 (user-error report))
163 value)))
164
165 ;;;###autoload
166 (defun validate-variable (symbol &optional noerror)
167 "Check that SYMBOL's value matches its schema.
168 SYMBOL must be the name of a custom option with a defined
169 `custom-type'. If SYMBOL has a value and a type, they are checked
170 with `validate-value'. NOERROR is passed to `validate-value'."
171 (let* ((val (symbol-value symbol))
172 (type (custom-variable-type symbol)))
173 (if type
174 (validate-value val type)
175 (if noerror val
176 (error "Variable `%s' has no custom-type." symbol)))))
177
178 ;;;###autoload
179 (defun validate-mark-safe-local (symbol)
180 "Mark SYMBOL as a safe local if its custom type is obeyed."
181 (put symbol 'safe-local-variable
182 (lambda (val)
183 (validate-value val (custom-variable-type symbol) 'noerror))))
184
185 (provide 'validate)
186 ;;; validate.el ends here