all messages for Emacs-related lists mirrored at yhetil.org
 help / color / mirror / code / Atom feed
From: phillip.lord@russet.org.uk (Phillip Lord)
To: Artur Malabarba <bruce.connor.am@gmail.com>
Cc: emacs-devel <emacs-devel@gnu.org>
Subject: Re: [ELPA] New package: validate.el
Date: Mon, 18 Apr 2016 15:17:19 +0100	[thread overview]
Message-ID: <8760vft30w.fsf@russet.org.uk> (raw)
In-Reply-To: <CAAdUY-KoV0QOyHO2Qkwjxyv9mQNgyULo+bun9us2MyiYURG0jg@mail.gmail.com> (Artur Malabarba's message of "Mon, 18 Apr 2016 03:42:41 -0300")


I've wanted something like this for years, so this is really nice.

Can I suggest something like:

(validate-setq cider-known-endpoints "whoops")

which errors or setq's as appropriate.

Phil

Artur Malabarba <bruce.connor.am@gmail.com> writes:

> I've written a package for validating values against custom-types,
> while providing useful messages when the validation fails.
> I'd like to add it to Elpa (hopefully I'm not reinventing the wheel here).
>
> Cheers,
> Artur
>
> ---
> ;;; validate.el --- Schema validation for Emacs-lisp  -*-
> lexical-binding: t; -*-
>
> ;; Copyright (C) 2016  Artur Malabarba
>
> ;; Author: Artur Malabarba <emacs@endlessparentheses.com>
> ;; Keywords: lisp
> ;; Package-Requires: ((emacs "24.1") (cl-lib "0.5"))
> ;; Version: 0.1
>
> ;;; 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.
> ;;
> ;; 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 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."
>   (if (not (= (length values) (length schemas)))
>       "wrong number of elements"
>     (seq-find #'identity (seq-mapn #'validate--check values schemas))))
>
> (defun validate--check (value schema)
>   "Return nil if VALUE matches SCHEMA.
> If they don't match, return an explanation."
>   (let ((args (cdr-safe schema))
>         (expected-type (or (car-safe schema) schema))
>         (props nil))
>     (while (and (keywordp (car args)) (cdr args))
>       (setq props `(,(pop args) ,(pop args) ,@props)))
>     (setq args (or (plist-get props :args)
>                    args))
>     (let ((r
>            (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)
>                (variable (cond ((wtype 'symbol))
>                                ((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 value "") t) nil)
>                              ((wtype 'string))
>                              (t "not a valid regexp")))
>                (repeat (cond
>                         ((or (not args) (cdr args)) (error "`repeat'
> needs exactly one argument"))
>                         ((wtype 'list))
>                         (t (let ((subschema (car args)))
>                              (seq-some (lambda (v) (validate--check v
> subschema)) value)))))
>                ((const function-item variable-item) (unless (eq value
> (car args))
>                                                       "not the expected value"))
>                (file (cond ((wtype 'string))
>                            ((file-exists-p value) nil)
>                            ((plist-get props :must-match) "file does not exist")
>                            ((not (file-writable-p value)) "file is not
> accessible")))
>                (directory (cond ((wtype 'string))
>                                 ((file-directory-p value) nil)
>                                 ((file-exists-p value) "path is not a
> directory")
>                                 ((not (file-writable-p value))
> "directory is not accessible")))
>                (key-sequence (and (wtype 'string)
>                                   (wtype 'vector)))
>                ;; TODO: `coding-system', `color'
>                (coding-system (wtype 'symbol))
>                (color (wtype 'string))
>                (cons (or (wtype 'cons)
>                          (validate--check (car value) (car args))
>                          (validate--check (cdr value) (cadr args))))
>                ((list group) (or (wtype 'list)
>                                  (validate--check-list-contents value args)))
>                (vector (or (wtype 'vector)
>                            (validate--check-list-contents value args)))
>                (alist (let ((value-type (plist-get props :value-type))
>                             (key-type (plist-get props :key-type)))
>                         (cond ((not value-type) (error "`alist' needs
> a :value-type"))
>                               ((not key-type) (error "`alist' needs a
> :key-type"))
>                               ((wtype 'list))
>                               (t (validate--check value
>                                           `(repeat (cons ,key-type
> ,value-type)))))))
>                ;; TODO: `plist'
>                ((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  "))))))
>                ;; TODO: `restricted-sexp'
>                (set (or (wtype 'list)
>                         (let ((failed (list t)))
>                           (dolist (schema args)
>                             (let ((elem (seq-find (lambda (x) (not
> (validate--check x schema)))
>                                                 value
>                                                 failed)))
>                               (unless (eq elem failed)
>                                 (setq value (remove elem value)))))
>                           (when value
>                             (concat "the following values don't match
> any of the options:\n  "
>                                     (mapconcat (lambda (x) (format
> "%s" x)) value "\n  "))))))))))
>       (when r
>         (let ((print-length 4)
>               (print-level 2))
>           (format "Looking for `%S' in `%S' failed because:\n%s"
>                   schema value r))))))
>
> ;;; Exposed API
> ;;;###autoload
> (defun validate-value (value schema &optional noerror)
>   "Check that VALUE matches SCHEMA.
> If it matches return VALUE, otherwise signal a `user-error'.
>
> If NOERROR is non-nil, return t to indicate a match and nil to
> indicate a failure."
>   (let ((report (validate--check value schema)))
>     (if report
>         (unless noerror
>           (user-error report))
>       value)))
>
> ;;;###autoload
> (defun validate-variable (symbol &optional noerror)
>   "Check that SYMBOL's value matches its schema.
> SYMBOL must be the name of a custom option with a defined
> `custom-type'. If SYMBOL has a value and a type, they are checked
> with `validate-value'. NOERROR is passed to `validate-value'."
>   (let* ((val (symbol-value symbol))
>          (type (custom-variable-type symbol)))
>     (if type
>         (validate-value val type)
>       (if noerror val
>         (error "Variable `%s' has no custom-type." symbol)))))
>
> (provide 'validate)
> ;;; validate.el ends here



  parent reply	other threads:[~2016-04-18 14:17 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2016-04-18  6:42 [ELPA] New package: validate.el Artur Malabarba
2016-04-18 12:30 ` Stefan Monnier
2016-04-18 15:10   ` Artur Malabarba
2016-04-18 14:17 ` Phillip Lord [this message]
2016-04-18 15:12   ` Artur Malabarba

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=8760vft30w.fsf@russet.org.uk \
    --to=phillip.lord@russet.org.uk \
    --cc=bruce.connor.am@gmail.com \
    --cc=emacs-devel@gnu.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this external index

	https://git.savannah.gnu.org/cgit/emacs.git
	https://git.savannah.gnu.org/cgit/emacs/org-mode.git

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.