;;; validate.el --- Schema validation for Emacs-lisp -*- lexical-binding: t; -*-
-;; Copyright (C) 2016 Artur Malabarba
+;; Copyright (C) 2016 Free Software Foundation, Inc.
;; Author: Artur Malabarba <emacs@endlessparentheses.com>
;; Keywords: lisp
+;; Package-Requires: ((emacs "24.1") (cl-lib "0.5"))
+;; Version: 0.4
-;; This program is free software; you can redistribute it and/or modify
+;;; Commentary:
+;;
+;; This library offers two functions that perform schema validation.
+;; Use this is your Elisp packages to provide very informative error
+;; messages when your users accidentally misconfigure a variable.
+;; For instance, if everything is fine, these do the same thing:
+;;
+;; 1. (validate-variable 'cider-known-endpoints)
+;; 2. cider-known-endpoints
+;;
+;; However, if the user has misconfigured this variable, option
+;; 1. will immediately give them an informative error message, while
+;; option 2. won't say anything and will lead to confusing errors down
+;; the line.
+;;
+;; The format and language of the schemas is the same one used in the
+;; `:type' property of a `defcustom'.
+;;
+;; See: (info "(elisp) Customization Types")
+;;
+;; Both functions throw a `user-error' if the value in question
+;; doesn't match the schema, and return the value itself if it
+;; matches. The function `validate-variable' verifies whether the value of a
+;; custom variable matches its custom-type, while `validate-value' checks an
+;; arbitrary value against an arbitrary schema.
+;;
+;; Missing features: `:inline', `plist', `coding-system', `color',
+;; `hook', `restricted-sexp'.
+
+;;; License:
+;;
+;; This file is part of GNU Emacs.
+;;
+;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
-
-;; This program is distributed in the hope that it will be useful,
+;;
+;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
-
-;; You should have received a copy of the GNU General Public License
-;; along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-;;; Commentary:
-
;;
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
;;; Code:
+(require 'cl-lib)
+(require 'seq)
+(require 'cus-edit)
(defun validate--check-list-contents (values schemas)
"Check that all VALUES match all SCHEMAS."
"wrong number of elements"
(seq-find #'identity (seq-mapn #'validate--check values schemas))))
+(defun validate--indent-by-2 (x)
+ (replace-regexp-in-string "^" " " x))
+
(defun validate--check (value schema)
"Return nil if VALUE matches SCHEMA.
If they don't match, return an explanation."
- (let ((fail (list schema value))
- (args (cdr-safe schema))
+ (let ((args (cdr-safe schema))
(expected-type (or (car-safe schema) schema))
(props nil))
(while (and (keywordp (car args)) (cdr args))
(setq args (or (plist-get props :args)
args))
(let ((r
- (cl-labels ((wtype ;wrong-type
- (t) (unless (funcall (intern (format "%sp" t)) value)
- (format "not a %s" t))))
+ (cl-labels ((wtype ;wrong-type
+ (tt) (unless (funcall (intern (format "%sp" tt)) value)
+ (format "not a %s" tt))))
;; TODO: hook (top-level only).
(cl-case expected-type
((sexp other) nil)
((not (boundp value)) "this symbol has no variable binding")))
((integer number float string character symbol function boolean face)
(wtype expected-type))
- (regexp (cond ((ignore-errors (string-match re "") t) nil)
+ (regexp (cond ((ignore-errors (string-match value "") t) nil)
((wtype 'string))
(t "not a valid regexp")))
(repeat (cond
(t (validate--check value
`(repeat (cons ,key-type ,value-type)))))))
;; TODO: `plist'
- ((choice radio) (if (not (cdr choice))
+ ((choice radio) (if (not (cdr args))
(error "`choice' needs at least one argument")
(let ((gather (mapcar (lambda (x) (validate--check value x)) args)))
(when (seq-every-p #'identity gather)
- (concat "all of the options failed\n "
- (mapconcat #'identity gather "\n "))))))
+ (concat "all of the options failed\n"
+ (mapconcat #'validate--indent-by-2 gather "\n"))))))
;; TODO: `restricted-sexp'
(set (or (wtype 'list)
(let ((failed (list t)))
(let ((print-length 4)
(print-level 2))
(format "Looking for `%S' in `%S' failed because:\n%s"
- schema value r))))))
+ schema value
+ (if (string-match "\\`Looking" r)
+ r
+ (validate--indent-by-2 r))))))))
\f
;;; Exposed API
(if noerror val
(error "Variable `%s' has no custom-type." symbol)))))
+;;;###autoload
+(defun validate-mark-safe-local (symbol)
+ "Mark SYMBOL as a safe local if its custom type is obeyed."
+ (put symbol 'safe-local-variable
+ (lambda (val)
+ (validate-value val (custom-variable-type symbol) 'noerror))))
+
+(defmacro validate-setq (symbol value)
+ "Like `setq', but throw an error if validation fails.
+VALUE is validated against SYMBOL's custom type."
+ `(if (boundp ',symbol)
+ (setq ,symbol (validate-value ,value (custom-variable-type ',symbol)))
+ (user-error "Trying to validate a variable that's not defined yet: `%s'.\nYou need to require the package before validating"
+ ',symbol)))
+
(provide 'validate)
;;; validate.el ends here