├── .gitignore ├── .travis.yml ├── README.md ├── mystic-file-mixin.asd ├── mystic-fiveam-mixin.asd ├── mystic-gitignore-mixin.asd ├── mystic-library-template.asd ├── mystic-readme-mixin.asd ├── mystic-test.asd ├── mystic-travis-mixin.asd ├── mystic.asd ├── roswell └── mystic.ros ├── src ├── mystic.lisp └── util.lisp ├── t └── mystic.lisp └── templates ├── library ├── asdf.lisp ├── library.lisp └── source.lisp └── mixins ├── file └── file.lisp ├── fiveam ├── code.lisp ├── fiveam.lisp └── system.lisp ├── gitignore ├── gitignore.lisp └── gitignore.txt ├── readme ├── readme.lisp └── source.md └── travis ├── travis.lisp └── travis.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | *.fasl 2 | *.dx32fsl 3 | *.dx64fsl 4 | *.lx32fsl 5 | *.lx64fsl 6 | *.x86f 7 | *~ 8 | .#* -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: common-lisp 2 | sudo: false 3 | 4 | env: 5 | global: 6 | - PATH=~/.roswell/bin:$PATH 7 | - ROSWELL_BRANCH=master 8 | - ROSWELL_INSTALL_DIR=$HOME/.roswell 9 | - COVERAGE_EXCLUDE=t 10 | matrix: 11 | - LISP=sbcl-bin COVERALLS=true 12 | 13 | install: 14 | # Roswell & coveralls 15 | - curl -L https://raw.githubusercontent.com/snmsts/roswell/$ROSWELL_BRANCH/scripts/install-for-ci.sh | sh 16 | - git clone https://github.com/fukamachi/cl-coveralls ~/lisp/cl-coveralls 17 | 18 | cache: 19 | directories: 20 | - $HOME/.roswell 21 | - $HOME/.config/common-lisp 22 | 23 | before_script: 24 | - ros --version 25 | - ros config 26 | 27 | script: 28 | - ros -e '(ql:quickload (list :fiveam :cl-coveralls))' 29 | -e '(setf fiveam:*debug-on-error* t 30 | fiveam:*debug-on-failure* t)' 31 | -e '(setf *debugger-hook* 32 | (lambda (c h) 33 | (declare (ignore h)) 34 | (print c) 35 | (uiop:quit -1)))' 36 | -e '(ql:quickload :mystic-test)' 37 | -e '(mystic-test:run-tests)' 38 | 39 | notifications: 40 | email: 41 | - eudoxiahp@gmail.com 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mystic 2 | 3 | [![Build Status](https://travis-ci.org/roswell/mystic.svg?branch=master)](https://travis-ci.org/roswell/mystic) 4 | 5 | A modular project skeleton generator. 6 | 7 | # Overview 8 | 9 | Some features: 10 | 11 | * **Declarative:** Options are specified in a declarative interface, so you have 12 | have multiple front-ends (command line, web, etc.) to the project generator. 13 | 14 | * **Modular:** Most of the functionality common to different templates 15 | (e.g. Travis support, the `.gitignore` file) is implemented as mixins. 16 | 17 | # Usage 18 | 19 | ```lisp 20 | (mystic:render (make-instance 'mystic.template.library:library-template) 21 | (list :name "your-project" 22 | :author "You" 23 | :email "you@gmail.com" 24 | :license "MIT" 25 | :description "A one-line description of your project." 26 | :dependencies "clack, postmodern") 27 | #p"/home/you/code/your-project/") 28 | ``` 29 | 30 | I'll get around to writing an actual interface at some point. 31 | 32 | # License 33 | 34 | Copyright (c) 2016 Fernando Borretti (eudoxiahp@gmail.com) 35 | 36 | Licensed under the MIT License. 37 | -------------------------------------------------------------------------------- /mystic-file-mixin.asd: -------------------------------------------------------------------------------- 1 | (defsystem mystic-file-mixin 2 | :author "Fernando Borretti " 3 | :maintainer "Fernando Borretti " 4 | :license "MIT" 5 | :version "0.1" 6 | :depends-on (:mystic) 7 | :components ((:module "templates/mixins/file" 8 | :components 9 | ((:file "file")))) 10 | :description "A Mystic mixin to render a list of files with Mustache.") 11 | -------------------------------------------------------------------------------- /mystic-fiveam-mixin.asd: -------------------------------------------------------------------------------- 1 | (defsystem mystic-fiveam-mixin 2 | :author "Fernando Borretti " 3 | :maintainer "Fernando Borretti " 4 | :license "MIT" 5 | :version "0.1" 6 | :depends-on (:mystic 7 | :mystic-file-mixin) 8 | :components ((:module "templates/mixins/fiveam" 9 | :components 10 | ((:file "fiveam")))) 11 | :description "A Mystic mixin to add a FiveAM test system and test suite.") 12 | -------------------------------------------------------------------------------- /mystic-gitignore-mixin.asd: -------------------------------------------------------------------------------- 1 | (defsystem mystic-gitignore-mixin 2 | :author "Fernando Borretti " 3 | :maintainer "Fernando Borretti " 4 | :license "MIT" 5 | :version "0.1" 6 | :depends-on (:mystic 7 | :mystic-file-mixin) 8 | :components ((:module "templates/mixins/gitignore" 9 | :components 10 | ((:file "gitignore")))) 11 | :description "A Mystic mixin to add a .gitignore file.") 12 | -------------------------------------------------------------------------------- /mystic-library-template.asd: -------------------------------------------------------------------------------- 1 | (defsystem mystic-library-template 2 | :author "Fernando Borretti " 3 | :maintainer "Fernando Borretti " 4 | :license "MIT" 5 | :version "0.1" 6 | :depends-on (:mystic 7 | :mystic-readme-mixin 8 | :mystic-gitignore-mixin 9 | :mystic-fiveam-mixin 10 | :mystic-travis-mixin) 11 | :components ((:module "templates/library" 12 | :components 13 | ((:file "library")))) 14 | :description "A Mystic mixin to add a .travis.yml file.") 15 | -------------------------------------------------------------------------------- /mystic-readme-mixin.asd: -------------------------------------------------------------------------------- 1 | (defsystem mystic-readme-mixin 2 | :author "Fernando Borretti " 3 | :maintainer "Fernando Borretti " 4 | :license "MIT" 5 | :version "0.1" 6 | :depends-on (:mystic 7 | :mystic-file-mixin) 8 | :components ((:module "templates/mixins/readme" 9 | :components 10 | ((:file "readme")))) 11 | :description "A Mystic mixin to add a README.md file.") 12 | -------------------------------------------------------------------------------- /mystic-test.asd: -------------------------------------------------------------------------------- 1 | (defsystem mystic-test 2 | :author "Fernando Borretti" 3 | :license "MIT" 4 | :depends-on (:mystic 5 | :mystic-library-template 6 | :fiveam) 7 | :components ((:module "t" 8 | :components 9 | ((:file "mystic"))))) 10 | -------------------------------------------------------------------------------- /mystic-travis-mixin.asd: -------------------------------------------------------------------------------- 1 | (defsystem mystic-travis-mixin 2 | :author "Fernando Borretti " 3 | :maintainer "Fernando Borretti " 4 | :license "MIT" 5 | :version "0.1" 6 | :depends-on (:mystic 7 | :mystic-file-mixin) 8 | :components ((:module "templates/mixins/travis" 9 | :components 10 | ((:file "travis")))) 11 | :description "A Mystic mixin to add a .travis.yml file.") 12 | -------------------------------------------------------------------------------- /mystic.asd: -------------------------------------------------------------------------------- 1 | (defsystem mystic 2 | :author "Fernando Borretti " 3 | :maintainer "Fernando Borretti " 4 | :license "MIT" 5 | :version "0.1" 6 | :homepage "" 7 | :bug-tracker "" 8 | :source-control (:git "") 9 | :depends-on (:cl-mustache 10 | :split-sequence 11 | :anaphora 12 | :alexandria 13 | :ubiquitous 14 | :local-time) 15 | :components ((:module "src" 16 | :serial t 17 | :components 18 | ((:file "util") 19 | (:file "mystic")))) 20 | :description "A project skeleton generator." 21 | :long-description 22 | #.(uiop:read-file-string 23 | (uiop:subpathname *load-pathname* "README.md")) 24 | :in-order-to ((test-op (test-op mystic-test)))) 25 | -------------------------------------------------------------------------------- /roswell/mystic.ros: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | #|-*- mode:lisp -*-|# 3 | #| 4 | exec ros -L sbcl-bin -Q -- $0 "$@" 5 | |# 6 | (ql:quickload :mystic :silent t) 7 | (defparameter *options* '()) 8 | (defparameter *defalut-template* "library") 9 | 10 | (let ((path (merge-pathnames ".mystic" (user-homedir-pathname)))) 11 | (when (probe-file path) 12 | (load path))) 13 | 14 | (defun cmds (cmds) 15 | (loop for cmd in cmds 16 | for result = (remove #\Newline (uiop:run-program cmd :output :string :ignore-error-status t)) 17 | unless (zerop (length result)) 18 | do (return result))) 19 | 20 | (defun main (template &rest argv) 21 | (let (name) 22 | (if (and argv (not (eql (aref (first argv) 0) #\-))) 23 | (setq name (first argv) 24 | argv (cdr argv)) 25 | (setq name template 26 | template *defalut-template*)) 27 | (ql:quickload (format nil "mystic-~A-template" template)) 28 | (mystic:render 29 | (make-instance (read-from-string (format nil "mystic.template.~A:~A-template" template template))) 30 | (nconc (list :name name) 31 | (loop for (a b) on argv by #'cddr 32 | collect (read-from-string (format nil ":~A" (string-left-trim "-:" a))) 33 | collect b) 34 | *options* 35 | ;; the idea comes from eazy-project 36 | (list :author (cmds '("git config --global --get user.name" "whoami")) 37 | :email (cmds '("git config --global --get user.email" "echo $(whoami)@$(hostname)")))) 38 | (make-pathname :defaults *load-pathname* :name nil :type nil)))) 39 | -------------------------------------------------------------------------------- /src/mystic.lisp: -------------------------------------------------------------------------------- 1 | (uiop:define-package #:mystic 2 | (:use #:cl) 3 | (:import-from #:anaphora 4 | #:aif 5 | #:it) 6 | (:export #:option 7 | #:option-title 8 | #:option-name 9 | #:option-docstring 10 | #:option-required-p 11 | #:option-default) 12 | (:export #:prompt-option 13 | #:multiple-choice-option 14 | #:boolean-option 15 | #:make-option) 16 | (:export #:template 17 | #:template-name 18 | #:template-docstring 19 | #:template-options) 20 | (:export #:missing-required-option 21 | #:bad-option-value 22 | #:ask-about-option-without-default 23 | #:ask-about-value-from-config 24 | #:ask-about-defaults 25 | #:base-option-condition) 26 | (:export #:validate-options 27 | #:render-template 28 | #:render 29 | #:list-templates 30 | #:register-template)) 31 | (in-package #:mystic) 32 | 33 | ;;; Classes 34 | 35 | (defclass option () 36 | ((name :reader option-name 37 | :initarg :name 38 | :type keyword 39 | :documentation "The name of the option, a keyword, e.g. @c(:project-name).") 40 | (title :reader option-title 41 | :initarg :title 42 | :type string 43 | :documentation "The human-readable option name, e.g. @c(\"Project Name\").") 44 | (docstring :reader option-docstring 45 | :initarg :docstring 46 | :type string 47 | :documentation "The option's documentation string.") 48 | (requiredp :reader option-required-p 49 | :initarg :requiredp 50 | :initform nil 51 | :type boolean 52 | :documentation "Whether the option is required. False by default.") 53 | (processor :reader option-processor 54 | :initarg :processor 55 | :initform #'identity 56 | :type function 57 | :documentation "A function used to process the value given to the option.") 58 | (default :reader option-default 59 | :initarg :default 60 | :initform nil 61 | :documentation "The option's default value. This value will not be passed through an option processor.")) 62 | (:documentation "An option to a template.")) 63 | 64 | (defclass prompt-option (option) 65 | () 66 | (:documentation "An option that prompts the user for a value.")) 67 | 68 | (defclass multiple-choice-option (option) 69 | ((choices :reader option-choices 70 | :initarg :choices 71 | :type list 72 | :documentation "A list of strings.")) 73 | (:documentation "An option that gives the user a choice from a list.")) 74 | 75 | (defclass boolean-option (option) 76 | () 77 | (:documentation "A yes or no option.")) 78 | 79 | (defclass template () 80 | ((name :reader template-name 81 | :initarg :name 82 | :type string 83 | :documentation "The template's human-readable name.") 84 | (docstring :reader template-docstring 85 | :initarg :docstring 86 | :type string 87 | :documentation "The template's documentation string.") 88 | (options :reader template-options 89 | :initarg :options 90 | :type list 91 | :documentation "A list of template options.")) 92 | (:documentation "Represents a template.")) 93 | 94 | ;;; Errors 95 | 96 | (define-condition base-option-condition () 97 | ((option-name :reader option-name 98 | :initarg :option-name 99 | :type keyword 100 | :documentation "The name of the option.") 101 | (docstring :reader option-docstring 102 | :initarg :docstring 103 | :type string 104 | :documentation "Documentation of the option."))) 105 | 106 | 107 | (define-condition ask-about-defaults (base-option-condition) 108 | ((option-default :reader option-default 109 | :initarg :option-default 110 | :documentation "The default value of the option.")) 111 | (:report 112 | (lambda (condition stream) 113 | (format stream "Value for option '~A' was not supplied.~@ 114 | ~@ 115 | This option should be ~S~@ 116 | ~@ 117 | Do you want to enter the value or to use default: ~S?" 118 | (option-name condition) 119 | (option-docstring condition) 120 | (option-default condition)))) 121 | (:documentation "Signalled when user passed REQUEST-ALL-OPTIONS-P argument to RENDER method and didn't supply some option value which has a default.")) 122 | 123 | 124 | (define-condition ask-about-value-from-config (base-option-condition) 125 | ((value :reader option-value 126 | :initarg :value 127 | :documentation "The value from config.") 128 | (config-path :reader config-path 129 | :initarg :config-path 130 | :documentation "Path to the config.")) 131 | (:report 132 | (lambda (condition stream) 133 | (format stream "Value for option '~A' was not supplied, but was found in config:~@ 134 | ~A~@ 135 | ~@ 136 | This option should be ~S~@ 137 | ~@ 138 | Do you want to enter another value or to use value from config: ~S?" 139 | (option-name condition) 140 | (config-path condition) 141 | (option-docstring condition) 142 | (option-value condition)))) 143 | (:documentation "Signalled when user passed REQUEST-ALL-OPTIONS-P argument to RENDER method, didn't supply some option value but a value for this option was found in a config file.")) 144 | 145 | 146 | (define-condition ask-about-option-without-default (base-option-condition) 147 | ((value :reader option-value 148 | :initarg :value 149 | :documentation "The value from config.")) 150 | (:report 151 | (lambda (condition stream) 152 | (format stream "Value for option '~A' was not supplied, and it has no default.~@ 153 | ~@ 154 | This option should be ~S~@ 155 | ~@ 156 | Do you want to enter another value or leave this option not filled?" 157 | (option-name condition) 158 | (option-docstring condition)))) 159 | (:documentation "Signalled when user passed REQUEST-ALL-OPTIONS-P argument to RENDER method, didn't supply an option value and we don't have a default or saved value for it.")) 160 | 161 | 162 | (define-condition missing-required-option (base-option-condition) 163 | () 164 | (:report 165 | (lambda (condition stream) 166 | (format stream "The option '~A' is required but was not supplied.~@ 167 | ~@ 168 | This option should be ~S." 169 | (option-name condition) 170 | (option-docstring condition)))) 171 | (:documentation "Signalled when a required option is not supplied.")) 172 | 173 | 174 | (define-condition bad-option-value (base-option-condition) 175 | ((value :reader option-value 176 | :initarg :value 177 | :initform (error "Argument :value is required.") 178 | :documentation "Wrong value of an option.") 179 | (problem :reader option-value-problem 180 | :initarg :problem 181 | :initform (error "Argument :problem is required.") 182 | :documentation "What is wrong with value.")) 183 | (:report 184 | (lambda (condition stream) 185 | (format stream "Invalid value ~S was given for option '~A'.~@ 186 | ~@ 187 | ~A" 188 | (option-value condition) 189 | (option-name condition) 190 | (option-value-problem condition)))) 191 | (:documentation "Signalled when a some value is in wrong format or type.")) 192 | 193 | ;;; Methods 194 | 195 | (defun read-new-value () 196 | (format *query-io* "~&Enter a new value: ") 197 | (finish-output *query-io*) 198 | (multiple-value-list (eval (read *query-io*)))) 199 | 200 | 201 | (defun config-pathname () 202 | (ubiquitous:designator-pathname ubiquitous:*storage-pathname* 203 | ubiquitous:*storage-type*)) 204 | 205 | 206 | (defgeneric validate-options (template options &key request-all-options-p) 207 | (:documentation "Validates templates and returns a plist where keys are option names. This plist will be used to render templates. 208 | 209 | A template or mixin might define a method for this generic function either to 210 | apply some additional validation or to add calculated variables to the plist. For example, 211 | if some option contains a system name, you might want to uppercase it or to replace 212 | dashes with underscores and store results to a separate variable.")) 213 | 214 | 215 | (defmethod validate-options ((template template) (options list) &key request-all-options-p) 216 | "Take a plist of options, and validate it against the options in the template. 217 | 218 | When ASK-ABOUT-DEFAULTS-P is T, Mystic will ask user about all options missing from OPTIONS list 219 | but used in TEMPLATE. Asking is performed as condition signaling and a user can choose one of 220 | available restarts: 221 | 222 | - USE-DEFAULT - use default specified in the option's code. 223 | - USE-VALUE-FROM-CONFIG - use value from config. 224 | - USE-VALUE - enter a value interactively. 225 | - STORE-VALUE - enter a value and store it in the config for future calls. 226 | " 227 | (ubiquitous:with-local-storage ('options) 228 | (loop with config-path = (config-pathname) 229 | with all-options = (template-options template) 230 | for option in all-options 231 | for name = (option-name option) 232 | for docstring = (option-docstring option) 233 | for value = (getf options (option-name option)) 234 | for default = (option-default option) 235 | for required = (option-required-p option) 236 | for value-from-config = (ubiquitous:value name) 237 | append (list name 238 | (restart-case 239 | (cond 240 | ;; Was the option supplied? If so, apply the option's processor to it 241 | ;; and add it to the `final-options` list 242 | (value 243 | (funcall (option-processor option) 244 | value)) 245 | (value-from-config 246 | (if request-all-options-p 247 | (error 'ask-about-value-from-config 248 | :option-name name 249 | :docstring docstring 250 | :value value-from-config 251 | :config-path config-path) 252 | value-from-config)) 253 | ;; Otherwise, check if it has a default value? 254 | ;; Then use this value: 255 | (default (if request-all-options-p 256 | (error 'ask-about-defaults 257 | :option-name name 258 | :docstring docstring 259 | :option-default default) 260 | default)) 261 | ;; No default? If the option is required, signal an error 262 | (required 263 | (error 'missing-required-option 264 | :option-name name 265 | :docstring docstring)) 266 | ;; When option is not required and there is no default, 267 | ;; but user wants to decide about all options: 268 | (request-all-options-p 269 | (error 'ask-about-option-without-default 270 | :option-name name 271 | :docstring docstring))) 272 | (continue () 273 | :report "Leave this option unfilled." 274 | :test (lambda (condition) 275 | (declare (ignore condition)) 276 | ;; Required options always should be filled, because 277 | ;; otherwise template might not render propertly. 278 | ;; That is why we don't provide this restart for required options. 279 | (unless required 280 | t)) 281 | nil) 282 | (use-default () 283 | :report (lambda (stream) 284 | (format stream "Use default value ~S." 285 | default)) 286 | :test (lambda (condition) 287 | (declare (ignore condition)) 288 | (not (null default))) 289 | default) 290 | (use-value-from-config () 291 | :report (lambda (stream) 292 | (format stream "Use value from config ~S (~A)." 293 | value-from-config 294 | config-path)) 295 | :test (lambda (condition) 296 | (declare (ignore condition)) 297 | (not (null value-from-config))) 298 | value-from-config) 299 | (use-value (option-value) 300 | :report "Provide a value." 301 | :interactive read-new-value 302 | option-value) 303 | (store-value (option-value) 304 | :report (lambda (stream) 305 | (format stream "Provide a value and save it for future renders to ~A." 306 | config-path)) 307 | :interactive read-new-value 308 | (setf (ubiquitous:value name) 309 | option-value) 310 | option-value)))))) 311 | 312 | (defgeneric render-template (template options directory) 313 | (:documentation "Render a @cl:param(template) to a @cl:param(directory). The 314 | @cl:param(options) are a plist of option names to their supplied values.") 315 | 316 | (:method-combination progn)) 317 | 318 | (defun render (template options directory &key request-all-options-p) 319 | "Render a @cl:param(template) to a @cl:param(directory). The 320 | @cl:param(options) are a plist of option names to their supplied values. 321 | 322 | This is the user-level entrypoint, and performs option validation and anything 323 | that needs to be done before handing over the task of rendering to the actual 324 | @c(render-template) method." 325 | (declare (type list options) 326 | (type pathname directory)) 327 | (let ((options (validate-options template options 328 | :request-all-options-p request-all-options-p)) 329 | (directory (uiop:ensure-directory-pathname directory))) 330 | (setf (getf options :year) 331 | (write-to-string (local-time:timestamp-year (local-time:now)))) 332 | (ensure-directories-exist directory) 333 | (render-template template options directory) 334 | (values))) 335 | 336 | (defvar *templates* (list) 337 | "The list of templates.") 338 | 339 | (defun list-templates () 340 | "Return a list of project templates." 341 | *templates*) 342 | 343 | (defun register-template (template) 344 | "Register a template." 345 | (declare (type template template)) 346 | (pushnew template *templates* :test #'(lambda (a b) 347 | (eq (class-name (class-of a)) 348 | (class-name (class-of b))))) 349 | template) 350 | 351 | 352 | (defun make-option (name title docstring &key requiredp (processor #'identity) default) 353 | (make-instance 'prompt-option 354 | :name name 355 | :title title 356 | :requiredp requiredp 357 | :docstring docstring 358 | :processor processor 359 | :default default)) 360 | 361 | 362 | (defmethod print-object ((obj prompt-option) stream) 363 | (print-unreadable-object (obj stream :type t) 364 | (loop with first-item-p = t 365 | for slot in '(name requiredp default docstring) 366 | for value = (when (slot-boundp obj slot) 367 | (slot-value obj slot)) 368 | when value 369 | do (if first-item-p 370 | (setf first-item-p nil) 371 | (write-char #\Space stream)) 372 | (format stream "~S ~A" 373 | (alexandria:make-keyword slot) 374 | value)))) 375 | -------------------------------------------------------------------------------- /src/util.lisp: -------------------------------------------------------------------------------- 1 | (uiop:define-package #:mystic.util 2 | (:use #:cl) 3 | (:export #:read-template-file 4 | #:strip-whitespace 5 | #:parse-comma-separated-list 6 | #:parse-systems-list 7 | #:render-string 8 | #:write-file) 9 | (:documentation "Utilities for Mystic.")) 10 | (in-package #:mystic.util) 11 | 12 | (defun read-template-file (pathname &key (asdf-system :mystic)) 13 | "Read a pathname relative to the templates/ directory into a string." 14 | (assert (uiop:relative-pathname-p pathname)) 15 | (uiop:read-file-string 16 | (merge-pathnames pathname 17 | (asdf:system-relative-pathname asdf-system #p"templates/")))) 18 | 19 | (defun strip-whitespace (string) 20 | (string-trim '(#\Space #\Tab) string)) 21 | 22 | (defun parse-comma-separated-list (systems-list) 23 | (loop for system-name 24 | in (split-sequence:split-sequence #\, systems-list) 25 | collecting 26 | (strip-whitespace system-name))) 27 | 28 | (defun parse-systems-list (systems-list) 29 | ;; TODO: leaved for backward compatibility 30 | (parse-comma-separated-list systems-list)) 31 | 32 | (defun render-string (template-string data) 33 | "Render a Mustache template string to a string." 34 | (with-output-to-string (str) 35 | (mustache:render 36 | template-string 37 | (loop for sublist on data by #'cddr collecting 38 | (cons (first sublist) (second sublist))) 39 | str))) 40 | 41 | (defun write-file (string pathname) 42 | "Write a string to a file (absolute path), creating it if necessary, and 43 | creating its parent directories as well." 44 | (assert (uiop:absolute-pathname-p pathname)) 45 | (ensure-directories-exist (uiop:pathname-directory-pathname pathname)) 46 | (with-open-file (output-stream pathname 47 | :direction :output 48 | :if-exists :supersede 49 | :if-does-not-exist :create) 50 | (write-string string output-stream)) 51 | pathname) 52 | -------------------------------------------------------------------------------- /t/mystic.lisp: -------------------------------------------------------------------------------- 1 | (uiop:define-package #:mystic-test 2 | (:use #:cl #:fiveam) 3 | (:import-from #:mystic 4 | #:validate-options) 5 | (:export #:run-tests)) 6 | (in-package #:mystic-test) 7 | 8 | (def-suite mystic 9 | :description "Mystic tests") 10 | (in-suite mystic) 11 | 12 | (test util 13 | (is 14 | (equal (mystic.util:strip-whitespace " test ") 15 | "test")) 16 | (is 17 | (equal (mystic.util:parse-systems-list "a, b, c") 18 | (list "a" "b" "c")))) 19 | 20 | (test render 21 | (let* ((dir (asdf:system-relative-pathname :mystic 22 | #p"t/my-project")) 23 | (true-dir (uiop:ensure-directory-pathname dir))) 24 | (finishes 25 | (mystic:render (make-instance 'mystic.template.library:library-template) 26 | (list :name "my-project" 27 | :author "me" 28 | :license "MIT") 29 | dir)) 30 | (flet ((file (pathname) 31 | (asdf:system-relative-pathname :mystic 32 | (merge-pathnames pathname 33 | #p"t/my-project/")))) 34 | ;; Test all the files exist 35 | (is-true 36 | (probe-file (file #p"my-project.asd"))) 37 | (is-true 38 | (probe-file (file #p"my-project-test.asd"))) 39 | (is-true 40 | (probe-file (file #p"README.md"))) 41 | (is-true 42 | (probe-file (file #p".gitignore"))) 43 | (is-true 44 | (probe-file (file #p".travis.yml"))) 45 | (is-true 46 | (probe-file (file #p"src/my-project.lisp"))) 47 | (is-true 48 | (probe-file (file #p"t/my-project.lisp"))) 49 | ;; Test they contain what we want 50 | (flet ((has (string file) 51 | (is 52 | (integerp (search string (uiop:read-file-string (file file))))))) 53 | (dolist (str (list "defsystem my-project" 54 | ":author \"me\"" 55 | ":maintainer \"me\"" 56 | ":license \"MIT\"" 57 | ":depends-on ()")) 58 | (has str #p"my-project.asd")) 59 | (has "defsystem my-project-test" #p"my-project-test.asd") 60 | (has "# my-project" #p"README.md") 61 | (has "define-package #:my-project" #p"src/my-project.lisp") 62 | (has "define-package #:my-project-test" #p"t/my-project.lisp"))) 63 | (uiop:delete-directory-tree true-dir :validate t))) 64 | 65 | (defun run-tests () 66 | (run! 'mystic)) 67 | -------------------------------------------------------------------------------- /templates/library/asdf.lisp: -------------------------------------------------------------------------------- 1 | (defsystem {{name}} 2 | :author "{{#email}}{{author}} <{{email}}>{{/email}}{{^email}}{{author}}{{/email}}" 3 | :maintainer "{{#email}}{{author}} <{{email}}>{{/email}}{{^email}}{{author}}{{/email}}" 4 | :license "{{license}}" 5 | :version "0.1" 6 | :homepage "{{homepage}}" 7 | :bug-tracker "" 8 | :source-control (:git "") 9 | :depends-on ({{dependencies}}) 10 | :components ((:module "src" 11 | :serial t 12 | :components 13 | ((:file "{{name}}")))) 14 | :description "{{description}}" 15 | :long-description 16 | #.(uiop:read-file-string 17 | (uiop:subpathname *load-pathname* "README.md")) 18 | :in-order-to ((test-op (test-op {{name}}-test)))) 19 | -------------------------------------------------------------------------------- /templates/library/library.lisp: -------------------------------------------------------------------------------- 1 | (uiop:define-package #:mystic.template.library 2 | (:use #:cl) 3 | (:import-from #:mystic.util 4 | #:read-template-file 5 | #:parse-comma-separated-list) 6 | (:import-from #:mystic 7 | #:make-option) 8 | (:import-from #:mystic.template.file 9 | #:make-file) 10 | (:export #:library-template) 11 | (:documentation "A Mystic mixin to add a .travis.yml file.")) 12 | (in-package #:mystic.template.library) 13 | 14 | ;;; Classes 15 | 16 | (defclass library-template (mystic.template.file:file-mixin 17 | mystic.template.readme:readme-mixin 18 | mystic.template.gitignore:gitignore-mixin 19 | mystic.template.fiveam:fiveam-mixin 20 | mystic.template.travis:travis-mixin) 21 | () 22 | (:default-initargs 23 | :name "Library" 24 | :docstring "An empty Common Lisp library." 25 | :options 26 | (list (make-option :name 27 | "Name" 28 | "The project's name." 29 | :requiredp t) 30 | (make-option :author 31 | "Author" 32 | "The project author's name." 33 | :requiredp t) 34 | (make-option :email 35 | "Email" 36 | "The project author's email.") 37 | (make-option :homepage 38 | "Homepage" 39 | "The project's homepage.") 40 | (make-option :license 41 | "License" 42 | "The project's license string, e.g. 'MIT', 'GPLv3'." 43 | :requiredp t) 44 | (make-option :description 45 | "Description" 46 | "A short, one-line description of the project.") 47 | (make-option :dependencies 48 | "Dependencies" 49 | "The project's dependent systems, as a comma-separated list, e.g: 'local-time, lucerne, crane'." 50 | :processor (lambda (deps) 51 | (format nil "~{:~A~^~% ~}" 52 | (parse-comma-separated-list deps))))) 53 | :files 54 | (list 55 | (make-file :mystic 56 | "library/asdf.lisp" 57 | "{{name}}.asd") 58 | (make-file :mystic 59 | "library/source.lisp" 60 | "src/{{name}}.lisp"))) 61 | (:documentation "A Mystic class to create empty Common Lisp library.")) 62 | 63 | (mystic:register-template (make-instance 'library-template)) 64 | -------------------------------------------------------------------------------- /templates/library/source.lisp: -------------------------------------------------------------------------------- 1 | (uiop:define-package #:{{name}} 2 | (:use #:cl)) 3 | (in-package #:{{name}}) 4 | -------------------------------------------------------------------------------- /templates/mixins/file/file.lisp: -------------------------------------------------------------------------------- 1 | (uiop:define-package #:mystic.template.file 2 | (:use #:cl) 3 | (:import-from #:mystic.util 4 | #:render-string 5 | #:write-file) 6 | (:import-from #:mystic.util 7 | #:read-template-file) 8 | (:export #:file 9 | #:file-path 10 | #:file-content 11 | #:file-mixin 12 | #:make-file) 13 | (:documentation "A Mystic template mixin for rendering a list of files using Mustache.")) 14 | (in-package #:mystic.template.file) 15 | 16 | ;;; Classes 17 | 18 | (defclass file () 19 | ((path :reader file-path 20 | :initarg :path 21 | :type string 22 | :documentation "The path to the file relative to the directory, 23 | a Mustache template string.") 24 | (content :reader file-content 25 | :initarg :content 26 | :type string 27 | :documentation "The file's contents, a Mustache template string.")) 28 | (:documentation "A file.")) 29 | 30 | (defclass file-mixin (mystic:template) 31 | ((files :reader template-files 32 | :initarg :files 33 | :type list 34 | :documentation "A list of files to template.")) 35 | (:documentation "A Mystic template mixin for rendering a list of files using Mustache.")) 36 | 37 | ;;; Render 38 | 39 | (defmethod mystic:render-template progn ((template file-mixin) options directory) 40 | (declare (type list options) 41 | (type pathname directory)) 42 | (loop for file in (template-files template) do 43 | (let* ((file-path (parse-namestring (render-string (file-path file) options))) 44 | (full-file-path (merge-pathnames file-path 45 | directory)) 46 | (content (mystic.util:render-string (file-content file) options))) 47 | (write-file content full-file-path)))) 48 | 49 | (defun make-file (system-name path output-path-template) 50 | (make-instance 'file 51 | :path output-path-template 52 | :content (read-template-file path :asdf-system system-name))) 53 | -------------------------------------------------------------------------------- /templates/mixins/fiveam/code.lisp: -------------------------------------------------------------------------------- 1 | (uiop:define-package #:{{name}}-test 2 | (:use #:cl #:fiveam) 3 | (:export #:run-tests)) 4 | (in-package #:{{name}}-test) 5 | 6 | (def-suite tests 7 | :description "{{name}} tests.") 8 | (in-suite tests) 9 | 10 | (test simple-test 11 | (is 12 | (equal 1 1)) 13 | (is-true 14 | (and t t))) 15 | 16 | (defun run-tests () 17 | (run! 'tests)) 18 | -------------------------------------------------------------------------------- /templates/mixins/fiveam/fiveam.lisp: -------------------------------------------------------------------------------- 1 | (uiop:define-package #:mystic.template.fiveam 2 | (:use #:cl) 3 | (:import-from #:mystic.util 4 | #:read-template-file 5 | #:render-string 6 | #:write-file) 7 | (:export #:fiveam-mixin) 8 | (:documentation "A Mystic mixin to add a FiveAM test system and test suite.")) 9 | (in-package #:mystic.template.fiveam) 10 | 11 | ;;; Classes 12 | 13 | (defclass fiveam-mixin (mystic:template) 14 | () 15 | (:documentation "A Mystic mixin to add a FiveAM test system and test suite.")) 16 | 17 | ;;; Render 18 | 19 | (defmethod mystic:render-template progn ((template fiveam-mixin) options directory) 20 | (declare (type list options) 21 | (type pathname directory)) 22 | (let ((system (read-template-file #p"mixins/fiveam/system.lisp")) 23 | (source (read-template-file #p"mixins/fiveam/code.lisp"))) 24 | (write-file (render-string system options) 25 | (merge-pathnames (parse-namestring 26 | (render-string "{{name}}-test.asd" options)) 27 | directory)) 28 | (write-file (render-string source options) 29 | (merge-pathnames (parse-namestring 30 | (render-string "t/{{name}}.lisp" options)) 31 | directory)))) 32 | -------------------------------------------------------------------------------- /templates/mixins/fiveam/system.lisp: -------------------------------------------------------------------------------- 1 | (defsystem {{name}}-test 2 | :author "{{#email}}{{author}} <{{email}}>{{/email}}{{^email}}{{author}}{{/email}}" 3 | :license "{{license}}" 4 | :depends-on (:{{name}} 5 | :fiveam) 6 | :components ((:module "t" 7 | :serial t 8 | :components 9 | ((:file "{{name}}"))))) 10 | -------------------------------------------------------------------------------- /templates/mixins/gitignore/gitignore.lisp: -------------------------------------------------------------------------------- 1 | (uiop:define-package #:mystic.template.gitignore 2 | (:use #:cl) 3 | (:import-from #:mystic.util 4 | #:read-template-file 5 | #:render-string 6 | #:write-file) 7 | (:export #:gitignore-mixin) 8 | (:documentation "A Mystic mixin to add a .gitignore file to a project.")) 9 | (in-package #:mystic.template.gitignore) 10 | 11 | ;;; Classes 12 | 13 | (defclass gitignore-mixin (mystic:template) 14 | () 15 | (:documentation "A Mystic mixin to add a .gitignore file to a project.")) 16 | 17 | ;;; Render 18 | 19 | (defmethod mystic:render-template progn ((template gitignore-mixin) options directory) 20 | (declare (type list options) 21 | (type pathname directory)) 22 | (let ((string (read-template-file #p"mixins/gitignore/gitignore.txt"))) 23 | (write-file (render-string string options) 24 | (merge-pathnames #p".gitignore" 25 | directory)))) 26 | -------------------------------------------------------------------------------- /templates/mixins/gitignore/gitignore.txt: -------------------------------------------------------------------------------- 1 | *.fasl 2 | *.dx32fsl 3 | *.dx64fsl 4 | *.lx32fsl 5 | *.lx64fsl 6 | *.x86f 7 | *~ 8 | .#* -------------------------------------------------------------------------------- /templates/mixins/readme/readme.lisp: -------------------------------------------------------------------------------- 1 | (uiop:define-package #:mystic.template.readme 2 | (:use #:cl) 3 | (:import-from #:mystic.util 4 | #:read-template-file 5 | #:render-string 6 | #:write-file) 7 | (:export #:readme-mixin) 8 | (:documentation "A Mystic mixin to add a README.md file.")) 9 | (in-package #:mystic.template.readme) 10 | 11 | ;;; Classes 12 | 13 | (defclass readme-mixin (mystic:template) 14 | () 15 | (:documentation "A Mystic mixin to add a README.md file.")) 16 | 17 | ;;; Render 18 | 19 | (defmethod mystic:render-template progn ((template readme-mixin) options directory) 20 | (declare (type list options) 21 | (type pathname directory)) 22 | (let ((readme (read-template-file #p"mixins/readme/source.md"))) 23 | (write-file (render-string readme options) 24 | (merge-pathnames #p"README.md" 25 | directory)))) 26 | -------------------------------------------------------------------------------- /templates/mixins/readme/source.md: -------------------------------------------------------------------------------- 1 | # {{name}} 2 | {{#description}} 3 | 4 | {{description}} 5 | 6 | {{/description}} 7 | # Overview 8 | 9 | # Usage 10 | 11 | # License 12 | 13 | Copyright (c) {{year}} {{author}} 14 | 15 | Licensed under the {{license}} License. 16 | -------------------------------------------------------------------------------- /templates/mixins/travis/travis.lisp: -------------------------------------------------------------------------------- 1 | (uiop:define-package #:mystic.template.travis 2 | (:use #:cl) 3 | (:import-from #:mystic.util 4 | #:read-template-file 5 | #:render-string 6 | #:write-file) 7 | (:export #:travis-mixin) 8 | (:documentation "A Mystic mixin to add a .travis.yml file to a project.")) 9 | (in-package #:mystic.template.travis) 10 | 11 | ;;; Classes 12 | 13 | (defclass travis-mixin (mystic:template) 14 | () 15 | (:documentation "A Mystic mixin to add a .travis.yml file to a project.")) 16 | 17 | ;;; Render 18 | 19 | (defmethod mystic:render-template progn ((template travis-mixin) options directory) 20 | (declare (type list options) 21 | (type pathname directory)) 22 | (let ((string (read-template-file #p"mixins/travis/travis.yaml"))) 23 | (write-file (render-string string options) 24 | (merge-pathnames #p".travis.yml" 25 | directory)))) 26 | -------------------------------------------------------------------------------- /templates/mixins/travis/travis.yaml: -------------------------------------------------------------------------------- 1 | language: common-lisp 2 | sudo: false 3 | 4 | env: 5 | global: 6 | - PATH=~/.roswell/bin:$PATH 7 | - ROSWELL_BRANCH=master 8 | - ROSWELL_INSTALL_DIR=$HOME/.roswell 9 | - COVERAGE_EXCLUDE=t 10 | matrix: 11 | - LISP=sbcl-bin COVERALLS=true 12 | 13 | install: 14 | # Roswell & coveralls 15 | - curl -L https://raw.githubusercontent.com/snmsts/roswell/$ROSWELL_BRANCH/scripts/install-for-ci.sh | sh 16 | - git clone https://github.com/fukamachi/cl-coveralls ~/lisp/cl-coveralls 17 | 18 | cache: 19 | directories: 20 | - $HOME/.roswell 21 | - $HOME/.config/common-lisp 22 | 23 | before_script: 24 | - ros --version 25 | - ros config 26 | 27 | script: 28 | - ros -e '(ql:quickload (list :cl-coveralls))' 29 | -e '(ql:quickload :{{name}}-test)' 30 | -e '(setf fiveam:*debug-on-error* t 31 | fiveam:*debug-on-failure* t)' 32 | -e '(setf *debugger-hook* 33 | (lambda (c h) 34 | (declare (ignore h)) 35 | (print c) 36 | (uiop:quit -1)))' 37 | -e '(coveralls:with-coveralls () 38 | ({{name}}-test:run-tests))' 39 | 40 | {{#email}} 41 | notifications: 42 | email: 43 | - {{email}} 44 | {{/email}} 45 | --------------------------------------------------------------------------------