about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--.guix/modules/gn-guile.scm2
-rw-r--r--README.md4
-rwxr-xr-xbin/gn-guile38
-rwxr-xr-xgn-guile.sh3
-rw-r--r--gn/db/mysql.scm7
-rwxr-xr-x[-rw-r--r--]gn/db/sparql.scm82
-rw-r--r--manifest.scm7
-rw-r--r--web/config.scm86
-rw-r--r--web/webserver.scm149
10 files changed, 266 insertions, 113 deletions
diff --git a/.gitignore b/.gitignore
index 5f81cf8..26235cb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@ pheno.txt
 GWA.json
 K.json
 .aider*
+.config/**
diff --git a/.guix/modules/gn-guile.scm b/.guix/modules/gn-guile.scm
index 03f2b14..87e059e 100644
--- a/.guix/modules/gn-guile.scm
+++ b/.guix/modules/gn-guile.scm
@@ -8,7 +8,7 @@
 ;;
 
 (define-module (gn-guile)
-  #:use-module ((gn packages guile) #:select (gn-guile) #:prefix gn:)
+  #:use-module ((gn-machines genenetwork) #:select (gn-guile) #:prefix gn:)
   #:use-module (guix gexp)
   #:use-module (guix utils)
   #:use-module (guix packages)
diff --git a/README.md b/README.md
index 923b64e..2577c00 100644
--- a/README.md
+++ b/README.md
@@ -34,7 +34,7 @@ git remote add gn git.genenetwork.org:/home/git/public/gn-guile
 GNU Guile allows you to develop against a live running web server using emacs-geiser on port 1970. To try this fire up the web server from the `web` directory as
 
 ```sh
-guix shell -L ~/guix-bioinformatics -m manifest.scm --container --network --file=guix.scm -- guile -L . --fresh-auto-compile --listen=1970 -e main web/webserver.scm 8091
+guix shell -L ~/guix-bioinformatics --container --network --development --file=guix.scm -- guile -L . --fresh-auto-compile --listen=1970 -e main bin/gn-guile 8091
 ```
 
 By default the root points to the API:
@@ -55,7 +55,7 @@ We recommend checking the Guix documentation for manifests, channels and guix.sc
 To run a standalone server you should run without the listener on port 1970:
 
 ```
-guix shell -L ~/guix-bioinformatics -m manifest.scm --container --network --file=guix.scm -- guile -L . --fresh-auto-compile -e main web/webserver.scm 8091
+guix shell -L ~/guix-bioinformatics --container --network --file=guix.scm -- guile -L . --fresh-auto-compile -e main bin/gn-guile 8091
 ```
 
 ## Welcome to the world of interactive Lisp programming
diff --git a/bin/gn-guile b/bin/gn-guile
new file mode 100755
index 0000000..47bd259
--- /dev/null
+++ b/bin/gn-guile
@@ -0,0 +1,38 @@
+#!/usr/bin/env sh
+# -*- mode: scheme; -*-
+exec guile --no-auto-compile -e main -s "$0" "$@"
+!#
+
+;;; gn-guile --- GN Guile web service
+;;; Copyright © 2026 Frederick M. Muriithi <fredmanglis@gmail.com>
+;;;
+;;; This file is part of gn-guile
+;;;
+;;; gn-guile 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.
+;;;
+;;; gn-guile 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 gn-guile.  If not, see <https://www.gnu.org/licenses/>.
+
+(use-modules (config)
+             (web config)
+             (web gn-uri)
+             (web webserver))
+
+(define (main args)
+  (write (string-append "Starting Guile REST API " get-version " server!"))
+  (write args)
+  (newline)
+  (let* ((options (parse-cli-options args))
+         (listen (option-ref options 'port)))
+    (when (option-ref options 'write)
+      (options-write options))
+    (display `("listening on" ,listen))
+    (start-web-server "127.0.0.1" listen (cli-options->gn-guile-config options))))
diff --git a/gn-guile.sh b/gn-guile.sh
deleted file mode 100755
index 9341b26..0000000
--- a/gn-guile.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#! @SHELL@
-
-guile -e main web/webserver.scm "$@"
diff --git a/gn/db/mysql.scm b/gn/db/mysql.scm
index 623a726..8da7b60 100644
--- a/gn/db/mysql.scm
+++ b/gn/db/mysql.scm
@@ -37,9 +37,10 @@
     )))
 
 (define (call-with-db thunk)
-  (let [(db (db-open))]
-    (thunk db)
-    (dbi-close db)))
+  (let* [(db (db-open))
+         (result (thunk db))]
+    (dbi-close db)
+    result))
 
 (define (ensure db msg1)
   "Use DBI-style handle to report an error. On error the program will stop."
diff --git a/gn/db/sparql.scm b/gn/db/sparql.scm
index bd7a306..fbcd2cc 100644..100755
--- a/gn/db/sparql.scm
+++ b/gn/db/sparql.scm
@@ -18,10 +18,11 @@ the case.
   #:use-module (json)
   #:use-module (srfi srfi-1)
   #:use-module (web client)
+  #:use-module (web http)
   #:use-module (web gn-uri)
   #:use-module (web request)
+  #:use-module (web response)
   #:use-module (web uri)
-
   #:export (memo-sparql-species
             memo-sparql-species-meta
             sparql-species-meta
@@ -37,9 +38,11 @@ the case.
             strip-lang
             make-table
             make-pairs
-            )
-)
+	    sparql-http-get
+	    sparql-by-term))
 
+(define virtuoso-endpoint
+  (or (getenv "SPARQL-ENDPOINT") "http://localhost:8890/sparql/"))
 
 (define (strip-lang s)
   "Strip quotes and language tag (@en) from RDF entries"
@@ -58,9 +61,9 @@ the case.
 (define (gn-sparql-prefix query)
   (string-append
   "
-PREFIX gn:  <http://genenetwork.org/id/>
-PREFIX gnt: <http://genenetwork.org/term/>
-PREFIX gnc: <http://genenetwork.org/category/>
+PREFIX gn:  <http://rdf.genenetwork.org/v1/id/>
+PREFIX gnt: <http://rdf.genenetwork.org/v1/term/>
+PREFIX gnc: <http://rdf.genenetwork.org/v1/category/>
 PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
 
 " query))
@@ -108,46 +111,7 @@ Note this procedure works for GN, but does not yet work for wikidata"
 (define (tsv->scm text)
   "Split a TSV string into a list of fields. Returns list of names header) and rows"
   (let ([lst (map (lambda (f) (string-split f #\tab) ) (delete "" (string-split text #\newline)))])
-    (values (car lst) (cdr lst))
-  ))
-
-#!
-(define-values (names res) (sparql-species-meta))
-(define table (get-rows names res))
-(define recs '())
-(define h (compile-species recs table))
-(assoc "http://genenetwork.org/species_drosophila_melanogaster" h)
-(assoc-ref h "http://genenetwork.org/id/Drosophila_melanogaster")
-(define d (car h))
-(assoc-ref (list d) "http://genenetwork.org/species_drosophila_melanogaster")
-
-(scm->json #(1  (("2" . 3))))
-;; [1,{"2":3}]
-(scm->json #("http://genenetwork.org/species_drosophila_melanogaster" (("http://genenetwork.org/menuName" . "Drosophila") ("http://genenetwork.org/binomialName" . "Drosophila melanogaster") )))
-;; ["http://genenetwork.org/species_drosophila_melanogaster",{"http://genenetwork.org/menuName":"Drosophila","http://genenetwork.org/binomialName":"Drosophila melanogaster"}]
-l
-;; (("http://genenetwork.org/menuName" "Drosophila") ("http://genenetwork.org/name" "Drosophila") ("http://genenetwork.org/binomialName" "Drosophila melanogaster"))
-(scm->json (map (lambda (i) (cons (car i) (car (cdr i)))) l))
-;; {"http://genenetwork.org/menuName":"Drosophila","http://genenetwork.org/name":"Drosophila","http://genenetwork.org/binomialName":"Drosophila melanogaster"}
-
-
-curl -G https://query.wikidata.org/sparql -H "Accept: application/json; charset=utf-8" --data-urlencode query="SELECT DISTINCT * where {
-  wd:Q158695 wdt:P225 ?o .
-} limit 5"
-{
-  "head" : {
-    "vars" : [ "o" ]
-  },
-  "results" : {
-    "bindings" : [ {
-      "o" : {
-        "type" : "literal",
-        "value" : "Arabidopsis thaliana"
-      }
-    } ]
-  }
-}
-!#
+    (values (car lst) (cdr lst))))
 
 (define (sparql-wd-species-info species)
   "Returns wikidata entry for species, e.g.:
@@ -326,3 +290,29 @@ dump-species-metadata.ttl:gn:Axbxa gnt:belongsToSpecies gn:Mus_musculus .
             " gnid " ?key ?value .
             # FILTER ( !EXISTS{ " gnid " gnt:hasTissue ?value })
 }")))
+
+
+(define* (sparql-http-get endpoint-url query #:optional (mime-type "text/microdata+html"))
+  (receive (response-status response-body)
+      (http-request
+       (format #f "~a?default-graph-uri=&query=~a&format=~a"
+	       endpoint-url (uri-encode query) (uri-encode mime-type))
+       #:method 'GET)
+    (values
+     (build-response
+      #:code (response-code response-status)
+      #:headers `((content-type . ,(parse-header 'content-type mime-type))))
+     response-body)))
+
+(define (sparql-by-term prefix val)
+  (let ((url-alist '((gn . "<http://rdf.genenetwork.org/v1/id/>")
+		     (gnc . "<http://rdf.genenetwork.org/v1/category/>")
+		     (gnt . "<http://rdf.genenetwork.org/v1/term/>"))))
+    (format #f "PREFIX ~a: ~a
+
+CONSTRUCT {
+  ~a:~a ?p ?o .
+} FROM <http://rdf.genenetwork.org/v1>
+WHERE {
+  ~a:~a ?p ?o .
+}" prefix (assoc-ref url-alist prefix) prefix val prefix val)))
diff --git a/manifest.scm b/manifest.scm
index bc8f699..aa2b3be 100644
--- a/manifest.scm
+++ b/manifest.scm
@@ -1,4 +1,11 @@
 ;; please do not remove
+;; -----------------------
+;; Move these dependencies to `propagated-inputs' for the `gn-guile' package in
+;; guix-bioinformatics and remove this file.
+;;
+;; This will help avoid inconsistencies in the list of dependencies when doing
+;; development and when deploying.
+;; -----------------------
 (specifications->manifest
  '("coreutils"
    "guile"
diff --git a/web/config.scm b/web/config.scm
new file mode 100644
index 0000000..9b3b9c2
--- /dev/null
+++ b/web/config.scm
@@ -0,0 +1,86 @@
+;;; Copyright © 2026  Frederick M Muriithi <fredmanglis@gmail.com>
+
+(define-module (web config)
+  #:use-module (srfi srfi-9 gnu)
+
+  #:use-module (config)
+  #:use-module (config api)
+  #:use-module (config parser sexp)
+
+  #:export (<gn-guile-config>
+            gn-guile-config-port
+            gn-guile-config-gn-docs-remote-url
+            gn-guile-config-gn-docs-local-checkout
+            gn-guile-config-gn-docs-working-branch
+
+            parse-cli-options
+            cli-options->gn-guile-config))
+
+(define-immutable-record-type <gn-guile-config>
+  (gn-guile-config port gn-docs-remote-url gn-docs-local-checkout
+                   gn-docs-working-branch)
+  gn-guile-config?
+  (port gn-guile-config-port)
+  (gn-docs-remote-url gn-guile-config-gn-docs-remote-url)
+  (gn-docs-local-checkout gn-guile-config-gn-docs-local-checkout)
+  (gn-docs-working-branch gn-guile-config-gn-docs-working-branch))
+
+
+(define string->exact (compose inexact->exact string->number))
+
+
+(define (user-port? parsed)
+  (and (positive? parsed) (>= parsed 1024) (<= parsed 49151)))
+
+
+(define (parse-cli-options cmd-line)
+  "Read configuration values from files and command-line options and convert them to appropriate data types."
+  (let ((config
+         (configuration (name 'gn-guile)
+                        (synopsis "gn-guile web service: provide services
+ to main Genenetwork service.")
+                        (description "gn-guile web service is a small
+service, written in GNU Guile, that provides some functionality to the main
+Genenetwork service in the background. This is not meant for direct user
+interaction.")
+                        (keywords
+                         (list (switch (name 'write)
+                                       (default #f)
+                                       (test boolean?)
+                                       (character #f)
+                                       (synopsis "Write the settings to configuration file(s)")
+                                       (description "When this option is present, the configuration values, provided as command line option, will be written to the file path(s) that has/have been specified."))
+                               (setting (name 'port)
+                                        (default 8091)
+                                        (test user-port?)
+                                        (handler string->exact)
+                                        (character #\p)
+                                        (synopsis "Port number that the service will listen on"))
+                               (setting (name 'gn-docs-remote-url)
+                                        (default "git@git.genenetwork.org:/home/git/public/gn-docs")
+                                        (test string?)
+                                        (character #\r)
+                                        (synopsis "Remote URI for gn-docs repository"))
+                               (setting (name 'gn-docs-local-checkout)
+                                        (default (string-append (dirname (getcwd)) "/gn-guile-files/gn-docs"))
+                                        (test file-exists?)
+                                        (character #\c)
+                                        (synopsis "Path where gn-docs is checked out"))
+                               (setting (name 'gn-docs-working-branch)
+                                        (default "non-existent")
+                                        (test string?)
+                                        (character #\b)
+                                        (synopsis "Branch to push/pull from"))))
+                        (parser sexp-parser)
+                        (directory (list (in-home ".config/gn-guile/")
+                                         (in-cwd ".config/"))))))
+    (getopt-config-auto cmd-line config)))
+
+
+(define (cli-options->gn-guile-config cli-options)
+  "Extract specific values from guile-config's <codex> object into gn-guile's custom configuration object."
+  (gn-guile-config
+   (option-ref cli-options 'port)
+   (option-ref cli-options 'gn-docs-remote-url)
+   (option-ref cli-options 'gn-docs-local-checkout)
+   (option-ref cli-options 'gn-docs-working-branch)))
diff --git a/web/webserver.scm b/web/webserver.scm
index 0c0bdd1..8c909a5 100644
--- a/web/webserver.scm
+++ b/web/webserver.scm
@@ -1,46 +1,43 @@
-(use-modules (json)
-             (ice-9 match)
-             (ice-9 format)
-             (ice-9 iconv)
-             (ice-9 receive)
-             (ice-9 string-fun)
-             (ice-9 exceptions)
-             (srfi srfi-1)
-             (srfi srfi-11)
-             (srfi srfi-13)
-             (srfi srfi-19)
-             (srfi srfi-26)
-             (rnrs io ports)
-             (rnrs bytevectors)
-             (web http)
-             (web client)
-             (web request)
-             (web response)
-             (web uri)
-             (web server)
-             (gn cache memoize)
-             (web gn-uri)
-             (gn db sparql)
-             (gn data dataset)
-             (gn data species)
-             (gn data group)
-             (gn runner gemma)
-             (web sxml)
-             (web view view)
-             (web view doc)
-             (web view markdown))
+(define-module (web webserver)
+  #:use-module (json)
+  #:use-module (ice-9 match)
+  #:use-module (ice-9 format)
+  #:use-module (ice-9 iconv)
+  #:use-module (ice-9 receive)
+  #:use-module (ice-9 string-fun)
+  #:use-module (ice-9 exceptions)
+  #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-11)
+  #:use-module (srfi srfi-13)
+  #:use-module (srfi srfi-19)
+  #:use-module (srfi srfi-26)
+  #:use-module (rnrs io ports)
+  #:use-module (rnrs bytevectors)
+  #:use-module (web http)
+  #:use-module (web client)
+  #:use-module (web request)
+  #:use-module (web response)
+  #:use-module (web uri)
+  #:use-module (web server)
+  #:use-module (gn cache memoize)
+  #:use-module (web gn-uri)
+  #:use-module (gn db sparql)
+  #:use-module (gn data dataset)
+  #:use-module (gn data species)
+  #:use-module (gn data group)
+  #:use-module (gn runner gemma)
+  #:use-module (web sxml)
+  #:use-module (web config)
+  #:use-module (web view view)
+  #:use-module (web view doc)
+  #:use-module (web view markdown)
+  #:export (start-web-server))
 
 (define (get-extension filename)
   (let ((dot-pos (string-rindex filename #\.)))
     (if dot-pos
         (substring filename dot-pos) "")))
 
-(define +current-repo-path+
-  (getenv "CURRENT_REPO_PATH"))
-
-(define +cgit-repo-path+
-  (getenv "CGIT_REPO_PATH"))
-
 (define +info+
   `(("name" . "GeneNetwork REST API") ("version" . ,get-version)
     ("comment" . "This is the official REST API for the GeneNetwork service hosted at https://genenetwork.org/")
@@ -181,7 +178,7 @@ otherwise search for set/group data"
     (cons (string->symbol (uri-decode key))
           (uri-decode value))))
 
-(define (edit-file-handler repo request)
+(define (edit-file-handler local-repo working-branch request)
   (catch 'file-error
          (lambda ()
            (let* ((query (uri-query (request-uri request)))
@@ -192,8 +189,12 @@ otherwise search for set/group data"
                   (query-path (assoc-ref params
                                          'file_path)))
              (if query-path
-                 (build-json-response 200
-                                      (fetch-file repo query-path))
+                 (begin
+                   (git-invoke local-repo "fetch" "origin" working-branch)
+                   (git-invoke local-repo "reset" "--hard"
+                               (string-append "origin/" working-branch))
+                   (build-json-response 200
+                                        (fetch-file local-repo query-path)))
                  (throw 'file-error
                         "Please provide a valid file path in the query"))))
          (lambda (key . args)
@@ -202,6 +203,24 @@ otherwise search for set/group data"
                                   `(("error" . ,key)
                                     ("msg" . ,msg)))))))
 
+(define (render-sparql request prefix val)
+  (let* ((mime (negotiate-mime request))
+	 (resp-mime (if (or (string-contains (symbol->string mime) "html")
+			    (string-contains (symbol->string mime) "microdata"))
+			'text/html
+			mime)))
+    (receive (sparql-header sparql-resp)
+	(sparql-http-get
+	 (or (getenv "SPARQL-ENDPOINT") "http://localhost:8890/sparql/")
+	 (sparql-by-term prefix val)
+	 (symbol->string mime))
+      (list `((content-type ,resp-mime))
+	    (lambda (port)
+	      (let ((resp (if (string? sparql-resp)
+			      sparql-resp
+			      (utf8->string sparql-resp))))
+		(put-string port resp)))))))
+
 (define (invalid-data? data target)
   (if (string? (assoc-ref data target))
       (if (string-null? (assoc-ref data target))
@@ -212,7 +231,7 @@ otherwise search for set/group data"
              (format #f "The Key  *** ~a *** is missing in your  Json Data"
                      target))))
 
-(define (commit-file-handler repo request body)
+(define (commit-file-handler repo-checkout remote-url request body)
   (catch 'system-error
          (lambda ()
 	   (let* ((post-data (decode-request-json body))
@@ -229,14 +248,14 @@ otherwise search for set/group data"
 	     (build-json-response 200
 				  ((lambda ()
 				     (let ((message
-					    (commit-file +current-repo-path+
+					    (commit-file repo-checkout
 							 file-name
 							 content
 							 commit-message
 							 username
 							 email
 							 prev-commit)))
-				       (git-invoke +current-repo-path+ "push" +cgit-repo-path+)
+				       (git-invoke repo-checkout "push" remote-url)
 				       message))))))
          (lambda (key . args)
            (let ((msg (car args)))
@@ -244,7 +263,15 @@ otherwise search for set/group data"
                                   `(("error" . ,key)
                                     ("msg" . ,msg)))))))
 
-(define (controller request body)
+(define (negotiate-mime request)
+  (let* ((headers (request-headers request))
+	 (accept (caar (assoc-ref headers 'accept))))
+    (if (or (eq? (string->symbol "*/*") accept)
+	    (eq? (string->symbol "text/html") accept))
+	'application/x-nice-microdata
+	accept)))
+
+(define (controller request body config)
   (match-lambda
     (('GET)
      (render-json +info+))
@@ -301,9 +328,13 @@ otherwise search for set/group data"
     (('GET "species")
      (render-json (get-species-meta)))
     (('GET "edit")
-     (edit-file-handler +current-repo-path+ request))
+     (edit-file-handler (gn-guile-config-gn-docs-local-checkout config)
+                        (gn-guile-config-gn-docs-working-branch config)
+                        request))
     (('POST "commit")
-     (commit-file-handler +current-repo-path+ request body))
+     (commit-file-handler (gn-guile-config-gn-docs-local-checkout config)
+                          (gn-guile-config-gn-docs-remote-url config)
+                          request body))
     (('GET id)
      (let ((names (get-species-shortnames (get-expanded-species))))
        (match (string->list id)
@@ -327,21 +358,31 @@ otherwise search for set/group data"
                 #\n)
           (render-json (get-id-data (list->string name))))
          (rest (render-json "NOP")))))
+    ;; RDF End-points
+    (('GET "v1" "id" id)
+     (render-sparql request 'gn id))
+
+    (('GET "v1" "category" category)
+     (render-sparql request 'gnc category))
+
+    (('GET "v1" "term" term)
+     (render-sparql request 'gnt term))
+
     (_ (not-found (request-uri request)))))
 
 (define (request-path-components request)
   (split-and-decode-uri-path (uri-path (request-uri request))))
 
-(define (handler request body)
+(define (handler request body config)
   (format #t "~a ~a\n"
           (request-method request)
           (uri-path (request-uri request)))
   (apply values
-         ((controller request body)
+         ((controller request body config)
           (cons (request-method request)
                 (request-path-components request)))))
 
-(define (start-web-server address port)
+(define (start-web-server address port config)
   (format (current-error-port)
           "GN REST API web server listening on http://~a:~a/~%" address port)
   ;; Wrap handler in another function to support live hacking via the
@@ -349,15 +390,7 @@ otherwise search for set/group data"
   ;; REPL, the web server will still be using the old handler. The
   ;; only way to update the handler reference held by the web server
   ;; would be to restart the web server.
-  (run-server (cut handler <> <>)
+  (run-server (cut handler <> <> config)
               'http
               (list #:addr (inet-pton AF_INET address)
                     #:port port)))
-
-(define (main args)
-  (write (string-append "Starting Guile REST API " get-version " server!"))
-  (write args)
-  (newline)
-  (let ((listen (inexact->exact (string->number (car (cdr args))))))
-    (display `("listening on" ,listen))
-    (start-web-server "127.0.0.1" listen)))