hackerbase/brmsaptool-orig.scm

413 lines
11 KiB
Scheme

;;
;; brmsaptool.scm
;;
;; ISC License
;;
;; Copyright 2023 Brmlab, z.s.
;; Dominik Pantůček <dominik.pantucek@trustica.cz>
;;
;; Permission to use, copy, modify, and/or distribute this software
;; for any purpose with or without fee is hereby granted, provided
;; that the above copyright notice and this permission notice appear
;; in all copies.
;;
;; THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
;; WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
;; WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
;; AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
;; CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
;; OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
;; NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
;; CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
;;
(import (chicken condition)
(chicken file)
(chicken pathname)
(chicken file posix)
(chicken io)
(chicken string)
(chicken format)
(chicken sort)
(chicken time)
(chicken time posix)
(chicken process-context)
testing
dictionary
month
period)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Static default configuration
(define *members-directory* (make-parameter "members"))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Testing
(define-syntax with-handler
(syntax-rules ()
((_ handler body ...)
(call-with-current-continuation
(lambda (k)
(with-exception-handler
(lambda (x) (k (handler x)))
(lambda () body ...)))))))
(define-syntax unit-test
(syntax-rules ()
((_ name condition)
(if (with-handler (lambda (x) #f)
condition)
(display ".")
(error 'unit-test name)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Member info data file
;; Member File Parser: remove comments from line and return the result
(define (mfp:line:remove-comments l)
(let ((si (substring-index "#" l)))
(if si
(if (= si 0)
""
(car (string-chop l si)))
l)))
;; Parses given key-value line. Key is up to first space, value is the
;; rest of the line. If the line doesn't contain anything, returns #f.
(define (parse-member-line l)
(let ((sp (string-split (mfp:line:remove-comments l) " ")))
(and sp
(not (null? sp))
(list (string->symbol (car sp))
(string-intersperse (cdr sp))))))
;; If given symbol represents start/stop symbol of either kind,
;; returns a list of the symbol representing the type and start/stop
;; symbol. It returns false otherwise.
(define (split-start/stop-symbol s)
(cond ((eq? s 'studentstart) '(student start))
((eq? s 'studentstop) '(student stop))
((eq? s 'suspendstart) '(suspend start))
((eq? s 'suspendstop) '(suspend stop))
(else #f)))
;; Processes member line adding given value v to the dictionary d
;; under key k. Special handling for start/stop symbols means given
;; value is prepended to given start/stop key (student/suspend) as
;; parsed month for later processing of student/suspend periods.
(define (process-member-line d k v)
(let ((ss (split-start/stop-symbol k)))
(if ss
(let ((pk (car ss))
(pd (cadr ss))
(vl (string-split v " ")))
(if (null? vl)
(error 'process-member-line "Missing date for start/stop symbol" k)
(let ((ds (car vl)))
(dict-set d pk
(cons (cons pd (string->month ds))
(dict-ref d pk '()))))))
(case k
((card desfire credit) (dict-set d k (cons v (dict-ref d k '()))))
(else
(dict-set d k v))))))
;; Converts given key in member info dictionary from period markers
;; list to periods.
(define (convert-member-key:markers->periods m k)
(dict-set m k
(period-markers->periods
(sort-period-markers
(dict-ref m k '())))))
;; Converts all given keys using period-markers->periods.
(define (convert-member-keys:markers->periods m . ks)
(let loop ((m m)
(ks ks))
(if (null? ks)
m
(loop (convert-member-key:markers->periods m (car ks))
(cdr ks)))))
;; Fills-in the defaults
(define (make-default-member-info)
(dict-set
(make-dict)
'joined
"2015-01"))
;; Processes all lines and returns a dictionary representing given
;; member.
(define (parse-member-lines ls)
(let loop ((ls ls)
(r (make-default-member-info)))
(if (null? ls)
(convert-member-keys:markers->periods r 'suspend 'student)
(let ((p (parse-member-line (car ls))))
(loop (cdr ls)
(if p
(apply process-member-line r p)
r))))))
;; Loads lines from given file in (*members-directory*) and parses
;; them.
(define (load-member-file fn)
(let* ((ffn (make-pathname (*members-directory*) fn))
(f (open-input-file ffn))
(ls (read-lines f))
(md (parse-member-lines ls)))
(display ".")
md))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Members database
;; Loads all symlinks from (*members-directory*) returning a list of
;; pairs (name . destination)
(define (load-members-raw-index)
(let loop ((fns (directory (*members-directory*)))
(rs '()))
(if (null? fns)
(let ()
(display "-")
rs)
(let* ((fn (car fns))
(ffn (make-pathname (*members-directory*) fn))
(sl (if (symbolic-link? ffn) (read-symbolic-link ffn) #f)))
(loop (cdr fns)
(if sl
(cons (cons fn sl) rs)
rs))))))
;; Converts the raw members index to a list of dictionaries with keys
;; 'id, 'name and 'file. File names are without directory element.
(define (expand-members-raw-index ri)
(let loop ((ri ri)
(ds '()))
(if (null? ri)
(let ()
(display "*")
ds)
(let* ((mp (car ri))
(lnk (car mp))
(dfn (cdr mp))
(lnkn (string->number lnk))
(dfnn (string->number dfn))
(id (or dfnn lnkn))
(name (if lnkn dfn lnk)))
(loop (cdr ri)
(cons (list (cons 'id id)
(cons 'name name)
(cons 'file dfn))
ds))))))
;; Adds the 'info key to all expanded index entries by loading
;; appropriate 'file key file from the members directory.
(define (load-members-from-expanded-index ei)
(let loop ((ei ei)
(mdb '()))
(if (null? ei)
mdb
(let ((mi (car ei)))
(let ((mid (load-member-file (dict-ref mi 'file))))
(loop (cdr ei)
(cons (dict-set mi
'info
mid)
mdb)))))))
;; Loads all member information from given members database.
(define (load-members)
(load-members-from-expanded-index
(expand-members-raw-index
(load-members-raw-index))))
;; Gets member based by generic predicate
(define (find-member-by-predicate mdb pred)
(let loop ((mdb mdb))
(if (null? mdb)
#f
(let ((mr (car mdb)))
(if (pred mr)
mr
(loop (cdr mdb)))))))
;; Gets member record by member key
(define (find-member-by-key mdb key val)
(find-member-by-predicate
mdb
(lambda (mr)
(equal? (dict-ref mr key) val))))
;; Gets member record by member id (from file/symlink)
(define (find-member-by-id mdb id)
(find-member-by-key mdb 'id id))
;; Gets member record by member name (from file/symlink)
(define (find-member-by-name mdb name)
(find-member-by-key mdb 'name name))
;; Gets member record by member nick in member file the key 'nick
(define (find-member-by-nick mdb nick)
(find-member-by-predicate
mdb
(lambda (mr)
(equal? (dict-ref (dict-ref mr 'info) 'nick) nick))))
;; Returns the list of all members ids
(define (list-members-ids mdb)
(map (lambda (mr) (dict-ref mr 'id)) mdb))
;; Returns the list of all file names in members database
(define (list-members-names mdb)
(map (lambda (mr) (dict-ref mr 'name)) mdb))
;; Returns the list of all members nicks
(define (list-members-nicks mdb)
(map (lambda (mr) (dict-ref (dict-ref mr 'info) 'nick)) mdb))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Member predicates
(define (member-suspended? mr)
#f)
(define (member-student? mr)
#f)
(define (member-destroyed? mr)
#f)
;; Returns true if the member is neither suspended nor destroyed
(define (member-active? mr)
(not (or (member-suspended? mr)
(member-destroyed? mr))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Command-line parsing
;; Prints simple help
(define (print-help)
(print "Command-line arguments:
-h prints this help
-M dir specifies the members database directory
-mi id member id
-mn nick member nick
")
(exit 0))
;; Consumes given number of arguments from the list and returns the
;; remainder of the list and a list of arguments consumed.
(define (consume-args args num)
(let loop ((args args)
(res '())
(num num))
(if (= num 0)
(list args (reverse res))
(if (null? args)
(error 'consume-args "Not enough arguments" num)
(loop (cdr args)
(cons (car args) res)
(- num 1))))))
;; Gets command-line arguments after the "--" of csi (not useful when
;; compiled)
(define (get-command-line-arguments)
(let* ((args (argv))
(rargs (member "--" args)))
(if rargs
(cdr rargs)
(cdr args))))
;; Performs the actual parsing based on specification.
(define (do-parse-command-line specs)
(let loop ((args (get-command-line-arguments)))
(when (not (null? args))
(let* ((arg (car args))
(specp (assoc arg specs)))
(when (not specp)
(error 'parse-command-line "Unknown argument" arg))
(let* ((proc (cadr specp))
(info (procedure-information proc))
(nargs (- (length info) 1))
(aargsl (consume-args (cdr args) nargs))
(args (car aargsl))
(aargs (cadr aargsl)))
(apply proc aargs)
(loop args))))))
;; Simple syntax wrapper for command-line arguments specification
(define-syntax parse-command-line
(syntax-rules ()
((_ ((arg proc) ...))
(do-parse-command-line
`((arg ,proc) ...)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Run everything
(define action (make-parameter #f))
(define member-parm (make-parameter #f))
;; Print banner
(print "brmsaptool 0.1 (c) 2023 Brmlab, z.s.")
(newline)
;; Handle options
(parse-command-line
(("-h" print-help)
("-M" (lambda (dn) (*members-directory* dn)))
("-mi" (lambda (id)
(action 'member-by-id)
(member-parm (string->number id))))
("-mn" (lambda (nick)
(action 'member-by-nick)
(member-parm nick)))
))
;; Run tests
(print "Running self-tests:")
(dictionary-tests!)
(month-tests!)
(period-tests!)
(print "All self-tests ok!")
(newline)
;; Load the members database
(display "Loading members ")
(define MDB (load-members))
(print " ok.")
(print "Members in database: " (length MDB))
(newline)
;; Perform requested action
(case (action)
((member-by-id member-by-nick)
(let ((mr (if (eq? (action) 'member-by-id)
(find-member-by-id MDB (member-parm))
(find-member-by-nick MDB (member-parm)))))
(if mr
(let* ((id (dict-ref mr 'id))
(name (dict-ref mr 'name))
(info (dict-ref mr 'info))
(sinfo (sort info
(lambda (a b)
(string<?
(symbol->string (car a))
(symbol->string (car b)))))))
(print "User " id " alias " name)
(let loop ((sinfo sinfo))
(when (not (null? sinfo))
(let* ((kv (car sinfo))
(k (car kv))
(v (cdr kv)))
(print " " k ":\t"
(if (member k '(student suspend))
(periods->string v)
v))
(loop (cdr sinfo))))))
(let ()
(print "No such member " (member-parm) ".")))))
((#f) (print "No action specified.")))