diff --git a/.gitignore b/.gitignore index bf4db1e..c4b839e 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ *.wx64fsl *.wx32fsl +*.directory diff --git a/LICENSE b/LICENSE index 204b93d..1723fbb 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -MIT License Copyright (c) +MIT License Copyright (c) 2021 Craig Oates Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 0fb9a43..445c55a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ -# ritherdon-rest +# Ritherdon REST -A CLI program which grabs data from the Return to Ritherdon project's REST API server. \ No newline at end of file +A CLI program which grabs data from the Return to Ritherdon project's REST API server. diff --git a/ritherdon-rest.asd b/ritherdon-rest.asd new file mode 100644 index 0000000..d789c89 --- /dev/null +++ b/ritherdon-rest.asd @@ -0,0 +1,34 @@ +;;;; ritherdon-rest.asd + +(asdf:defsystem #:ritherdon-rest + :description "Grabs data from the Return to Ritherdon project REST + API and prints out the results." + :author "craig@craigoates.net" + :license "MIT" + :version "0.0.1" + :serial t + :depends-on (:drakma + :cl-json) + :pathname "src/" + :components ((:file "package") + (:file "ritherdon-rest")) + :in-order-to ((test-op (test-op :ritherdon-rest/tests)))) + +;; The use of '...rest/tests' was because of a warning when trying to +;; run the code in SLIME. +(asdf:defsystem #:ritherdon-rest/tests + :description "The test suite for the ritherdon-rest project." + :author "craig@craigoates.net" + :license "MIT" + :version "0.0.1" + :serial t + :depends-on (:ritherdon-rest + :fiveam + :ratify) + :pathname "tests/" + :components ((:file "package") + (:file "main")) + :perform (test-op (o s) + (uiop:symbol-call :ritherdon-rest-tests :test-quasi))) + + diff --git a/src/package.lisp b/src/package.lisp new file mode 100644 index 0000000..1c4cfb1 --- /dev/null +++ b/src/package.lisp @@ -0,0 +1,7 @@ +;;;; package.lisp + +(defpackage #:ritherdon-rest + (:use #:cl #:drakma :cl-json) + (:export :parse-request + :decode-json)) ; Part of cl-json, getting errors without + ; it in SLIME. diff --git a/src/ritherdon-rest.lisp b/src/ritherdon-rest.lisp new file mode 100644 index 0000000..7c5a512 --- /dev/null +++ b/src/ritherdon-rest.lisp @@ -0,0 +1,37 @@ +;;;; ritherdon-rest.lisp + +;;; A collection of functions to aid in the make +;;; http-requests to the Ritherdon REST API. This project is help me +;;; learn Common Lisp. So, there is not much emphasis on being too +;;; strict on best practices and defensive coding. + +;;; NOTE: The Ritherdon REST API is part of an art exhibition at time +;;; of writing (29/06/2021). You will need to check if the server is +;;; still running if you want to run this code. + +(in-package #:ritherdon-rest) + +;; All the data provided by the Ritherdon REST API is in JSON. Becuase +;; of this I can set the `TEXT-CONTENT-TYPE' to JSON and keep it like +;; that until further notice. +(setq drakma:*text-content-types* (cons '("application" . "json") + drakma:*text-content-types*)) + +;; The Base URL which you build all your HTTP Requests on. You will +;; need to refer back to http://ritherdon.abbether.net/api/ui/ for the +;; full set of URL's. +(defvar *base-url* "http://ritherdon.abbether.net/api") + +;; The http-request is actually 'drakma:http-request'. I've done it +;; this way to highlight the feature. It is the same as what you've +;; used in the past with C# and 'using static' statements. So, not +;; much to go into here apart from highlighting it's a thing in Common +;; Lisp, too. +(defun make-request (query) + (http-request (concatenate 'string *base-url* query))) + +;; Same as the drakma example above. It can read as +;; 'cl-json:decode-json-from-string' but omitted the 'cl-json' +;; bit. You will find all package declarations in /src/package.lisp'. +(defun parse-request (query) + (decode-json-from-string(make-request query))) diff --git a/tests/main.lisp b/tests/main.lisp new file mode 100644 index 0000000..f89874c --- /dev/null +++ b/tests/main.lisp @@ -0,0 +1,255 @@ +;;;; tests/main.lisp + +(in-package #:ritherdon-rest-tests) + +(def-suite all-tests + :description "The master suite of all ritherdon-rest tests.") + +(in-suite all-tests) + +;;; HAD TO REVERT BACK TO FULL PACKAGE NAMES +;;; ======================================== + +;;; BELOW EXPLAINS HOW YOU CAN OMIT PACKAGE NAMES FROM FUNCTION +;;; CALLS. BUT, I'VE ADDED THE 'RATIFY' PACKAGE SINCE THEN AND I WAS +;;; GETTING NAMING CONFLICTS (WITH 'TEST'). SO, I'VE HAD TO USE THE +;;; FULL 'FIVEAM:TEST' FUNCTION CALLS. I'VE KEPT THE NOT FOR FUTURE +;;; REFERENCE AND ADDED THIS AS EXTRA CONTEXT FOR WHEN/HOW TO OMIT +;;; PACKAGE NAMES. + +;;; THIS BIT KINDA OUT-OF-DATE... + +;;; These two examples show the 'full' call to the fiveAm test +;;; functions. This is just for reference. The 'namespace' is already +;;; 'imported' in 'package.lisp'. + +;;; (fiveam:test sum-1 +;;; (fiveam:is (= 3 (+ 1 2)))) + +;;; (fiveam:run!) + +;;; How you would normally create the tests -- with fiveAM already +;;; set-up in 'package.lisp' and not needing to be explicit about it +;;; here. This is similar to 'using static' in C#. + +(defun test-quasi() + (run! 'all-tests)) + +;;; REST API Used Here is Temporary +;;; =============================== +;;; This REST API is a temporary one -- it's part of an artwork for +;;; the Return to Ritherdon project. The intention is to retire the +;;; API when the exhibition ends. So, these tests might fail because +;;; the API is no longer active. + + +;;; DEVICE STATUS TESTS +;;; =================== +;;; These tests are checking the 'shape' of the data returned by the +;;; HTTP (REST) request matches what is expected. These tests are +;;; expecting JSON data which looks similar to: + +;;; ((:ID . 79) (:STATUS . "off") (:TIME . "2021-07-01T16:00:001")). + +;;; ID: The row Id. in the database. +;;; STATUS: The current power state of the device (I.E. on or off). +;;; TIME: The timestamp when the status was recorded. + +;;; Usually, these updates are logged when each device is either just +;;; finished powering up or as they are about to power down. + +(fiveam:test factory-1-status-data + :description "Validates the (status) data produced by `FACTORY1'." + (let ((data (ritherdon-rest:parse-request "/status/latest/1"))) + ;; Data should look something like: + ;; ((:ID . 79) (:STATUS . "off") (:TIME . "2021-07-01T16:00:001")). + (is (= 3 (length data))) + (is (equal t (consp data))) + (is (equal ':id (first (nth 0 data)))) + (is (equal ':status (first (nth 1 data)))) + (is (equal ':time (first (nth 2 data)))) + (is (equal t (typep (cdr (car data)) 'integer))) ; row :id number + (is (> (cdr (car data)) 0)) + (is (equal t (typep (cdr (car (cdr data))) 'string))) ; :status value + (is (equal t (or (string-equal (cdr (car (cdr data))) "off") + (string-equal (cdr (car (cdr data))) "on")))))) + +(fiveam:test factory-2-status-data + :description "Validates the (status) data produced by `FACTORY2'." + (let ((data (ritherdon-rest:parse-request "/status/latest/2"))) + ;; Data should look something like: + ;; ((:ID . 79) (:STATUS . "off") (:TIME . "2021-07-01T16:00:001")). + (is (= 3 (length data))) + (is (equal t (consp data))) + (is (equal ':id (first (nth 0 data)))) + (is (equal ':status (first (nth 1 data)))) + (is (equal ':time (first (nth 2 data)))) + (is (equal t (typep (cdr (car data)) 'integer))) ; row :id number + (is (> (cdr (car data)) 0)) + (is (equal t (typep (cdr (car (cdr data))) 'string))) ; :status value + (is (equal t (or (string-equal (cdr (car (cdr data))) "off") + (string-equal (cdr (car (cdr data))) "on")))))) + +(fiveam:test factory-3-status-data + :description "Validates the (status) data produced by `FACTORY3'." + (let ((data (ritherdon-rest:parse-request "/status/latest/3"))) + ;; Ritherdon has a third welding booth but it was not included in + ;; the art project so no light sensor was installed. Therefore, + ;; this should always return the 'default/initial/seed' data + ;; readings. + ;; Data should still take the same shape as factory1 and factory 2: + ;; ((:ID . 1) (:STATUS . "off") (:TIME . "2021-04-26T20:42:19.400868")). + (is (= 3 (length data))) + (is (equal t (consp data))) + (is (equal ':id (first (nth 0 data)))) + (is (equal ':status (first (nth 1 data)))) + (is (equal ':time (first (nth 2 data)))) + (is (equal t (typep (cdr (car data)) 'integer))) ; row :id number + (is (> (cdr (car data)) 0)) + (is (equal t (typep (cdr (car (cdr data))) 'string))) ; :status value + (is (equal t (or (string-equal (cdr (car (cdr data))) "off") + (string-equal (cdr (car (cdr data))) "on")))))) + +(fiveam:test gallery-1-status-data + :description "Validates the (status) data produced by `GALLERY1'." + ;; Data should still take the same shape as others: + ;; ((:ID . 1) (:STATUS . "off") (:TIME . "2021-04-26T20:42:19.400868")) + (let ((data (ritherdon-rest:parse-request "/status/latest/4"))) + (is (= 3 (length data))) + (is (equal t (consp data))) + (is (equal ':id (first (nth 0 data)))) + (is (equal ':status (first (nth 1 data)))) + (is (equal ':time (first (nth 2 data)))) + (is (equal t (typep (cdr (car data)) 'integer))) ; row :id number + (is (> (cdr (car data)) 0)) + (is (equal t (typep (cdr (car (cdr data))) 'string))) ; :status value + (is (equal t (or (string-equal (cdr (car (cdr data))) "off") + (string-equal (cdr (car (cdr data))) "on")))))) + +(fiveam:test gallery-2-status-data + :description "Validates the (status) data produced by `GALLERY2'." + ;; Data should still take the same shape as others: + ;; ((:ID . 1) (:STATUS . "off") (:TIME . "2021-04-26T20:42:19.400868")) + (let ((data (ritherdon-rest:parse-request "/status/latest/5"))) + (is (= 3 (length data))) + (is (equal t (consp data))) + (is (equal ':id (first (nth 0 data)))) + (is (equal ':status (first (nth 1 data)))) + (is (equal ':time (first (nth 2 data)))) + (is (equal t (typep (cdr (car data)) 'integer))) ; row :id number + (is (> (cdr (car data)) 0)) + (is (equal t (typep (cdr (car (cdr data))) 'string))) ; :status value + (is (equal t (or (string-equal (cdr (car (cdr data))) "off") + (string-equal (cdr (car (cdr data))) "on")))))) + +(fiveam:test gallery-3-status-data + :description "Validates the (status) data produced by `GALLERY3'." + ;; This corresponds to 'factory3' -- no light sensor in welding + ;; booth 3 in Ritherdon. Therefore, no data to send. Because of + ;; this, the data returned here should be the 'seed data' unless + ;; I've manually updated it as part of another test. + ;; Data should still take the same shape as others: + ;; ((:ID . 1) (:STATUS . "off") (:TIME . "2021-04-26T20:42:19.400868")). + (let ((data (ritherdon-rest:parse-request "/status/latest/6"))) + (is (= 3 (length data))) + (is (equal t (consp data))) + (is (equal ':id (first (nth 0 data)))) + (is (equal ':status (first (nth 1 data)))) + (is (equal ':time (first (nth 2 data)))) + (is (equal t (typep (cdr (car data)) 'integer))) ; row :id number + (is (> (cdr (car data)) 0)) + (is (equal t (typep (cdr (car (cdr data))) 'string))) ; :status value + (is (equal t (or (string-equal (cdr (car (cdr data))) "off") + (string-equal (cdr (car (cdr data))) "on")))))) + + +;;; LIGHT READINGS TESTS +;;; ==================== + +;;; Theses tests check the 'shape' of the light meter readings +;;; returned by the HTTP (REST) requests. They make sure the data +;;; matches what's expected. These tests are expecting JSON data along +;;; the lines of: + +;;; ((:ID . 1855109) (:READING . 16) (:TIME . "2021-07-02T13:42:16")) + +;;; ID: The row Id. in the database. +;;; READING: The amount of light recorded. +;;; TIME: The timestamp when the reading was taken. + +;;; Because of how the light meters work, they do not always take +;;; light meter readings at consistent intervals. But, both devices +;;; aim to take a reading for as long as they are on. Also, Ritherdon +;;; has three welding booths and the system has the capacity to record +;;; all three booths. The Return to Ritherdon project decided on only +;;; recording two of the booths so 'factory3' should only return it +;;; seed data -- unless I've manually updated it (for another test +;;; most likely). + +(fiveam:test factory-1-reading-data ; Had to add fiveam here because + ; of naming conflict with ratify + ; package. Comment at top of file + ; explaining further. + :description "Validates the (light reading) data produced by `FACTORY1'." + ;; ((:ID . 1855109) (:READING . 16) (:TIME . "2021-07-02T13:42:16")) + (let ((data (ritherdon-rest:parse-request "/readings/latest/1"))) + (is-true (= 3 (length data))) + (is-true (consp data)) + (is (equal ':id (caar data))) + (is (equal ':reading (caadr data))) + (is (equal ':time (caaddr data))) + (is-true (> (cdar data) 0)) ; Db Id numbers start at 0. + (is-true (typep (cdadr data) 'integer)) ; reading can be <=> 0. + ;; Returns the date-time if data could be parsed. Returns `NIL' if + ;; it could not. Don't need to check if string. If system can't + ;; parse it doesn't matter what form the date-time is in. + (is-false (equal (ratify:datetime-p (cdaddr data)) nil)))) ; time value. + +(fiveam:test factory-2-reading-data + :description "Validates the (light reading) data produced by `FACTORY2'." + ;; ((:ID . 1855109) (:READING . 16) (:TIME . "2021-07-02T13:42:16")) + (let ((data (ritherdon-rest:parse-request "/readings/latest/2"))) + (is-true (= 3 (length data))) + (is-true (consp data)) + (is (equal ':id (caar data))) + (is (equal ':reading (caadr data))) + (is (equal ':time (caaddr data))) + (is-true (> (cdar data) 0)) ; Db Id numbers start at 0. + (is-true (typep (cdadr data) 'integer)) ; reading can be <=> 0. + ;; Returns the date-time if data could be parsed. Returns `NIL' if + ;; it could not. Don't need to check if string. If system can't + ;; parse it doesn't matter what form the date-time is in. + (is-false (equal (ratify:datetime-p (cdaddr data)) nil)))) ; time value. + +(fiveam:test factory-3-reading-data + :description "Validates the (light reading) data produced by `FACTORY3'." + ;; ((:ID . 1855109) (:READING . 16) (:TIME . "2021-07-02T13:42:16")) + (let ((data (ritherdon-rest:parse-request "/readings/latest/3"))) + (is-true (= 3 (length data))) + (is-true (consp data)) + (is (equal ':id (caar data))) + (is (equal ':reading (caadr data))) + (is (equal ':time (caaddr data))) + (is-true (> (cdar data) 0)) ; Db Id numbers start at 0. + (is-true (typep (cdadr data) 'integer)) ; reading can be <=> 0. + ;; This device is not active so the seed data should be + ;; returned. This data should not be produce `NIL' when Ratify + ;; tries to parse it (not a valid timestamp). If the timestamp can + ;; be parsed it most likely means I've updated the latest reading + ;; data (manually most likely) which should result in this failing + ;; because Ratify will return the parsed timestamp and not `NIL'. + (is (equal (ratify:datetime-p (cdaddr data)) nil)))) ; time value. + +(fiveam:test all-device-status-data + :description "Validates the status data produced when all status + data for all devices is requested in the same API call." + (let ((data (ritherdon-rest:parse-request "/status/latest"))) + (is (= 6 (length data))) + (is-true (consp data)) + (is (equal ':|DEVICE 1| (car (first data)))) + (is (equal ':|DEVICE 2| (car (second data)))) + (is (equal ':|DEVICE 3| (car (third data)))) + (is (equal ':|DEVICE 4| (car (fourth data)))) + (is (equal ':|DEVICE 5| (car (fifth data)))) + ;; if 6 not last a big change has occurred. + (is (equal ':|DEVICE 6| (caar (last data)))))) diff --git a/tests/package.lisp b/tests/package.lisp new file mode 100644 index 0000000..08b4fcc --- /dev/null +++ b/tests/package.lisp @@ -0,0 +1,9 @@ +;;;; tests/package.lisp + +(defpackage #:ritherdon-rest-tests + (:use #:cl + #:fiveam + #:ratify) + (:export #:run! + #:all-tests + #:test-quasi))