biome/biome-api.el
2023-06-28 15:38:38 +03:00

211 lines
8.4 KiB
EmacsLisp
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

;;; biome-api.el --- API for Open Meteo -*- lexical-binding: t -*-
;; Copyright (C) 2023 Korytov Pavel
;; Maintainer: Korytov Pavel <thexcloud@gmail.com>
;; Homepage: https://github.com/SqrtMinusOne/biome
;; This file is NOT part of GNU Emacs.
;; This program 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.
;; This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; TODO
;;; Code:
(require 'request)
(require 'cl-lib)
(require 'seq)
;; Parsing
(defconst biome-api--parse-exclude-ids
'("select_city" "current_weather" "localhost")
"Exclude these IDs from parsing.")
(defconst biome-api--replace-variables
`(("past_days" . ((:name . "Past days")
(:type . number)
(:min . 0)
(:max . 92)))
("forecast_days" . ((:name . "Forecast days")
(:type . number)
(:min . 0)
(:max . 16)))
("start_date" . ((:name . "Start date")
(:type . date)))
("end_date" . ((:name . "End date")
(:type . date)))
("timezone" . ((:name . "Timezone")
(:type . timezone))))
"Replace these variable defintions with the given ones.")
(defun biome-api--fix-string (string)
"Remove extra spaces and newlines from STRING."
(string-trim
(replace-regexp-in-string
(rx (+ (or "\n" "\t" " " " " " ")))
" "
string)))
(defun biome-api--parse-page-variables (section)
"Parse variables from SECTION.
SECTION is a DOM element."
(let ((elements (dom-search section (lambda (el) (not (stringp el)))))
fields field-names)
(cl-loop for elem in elements
if (member (dom-attr elem 'id) biome-api--parse-exclude-ids)
do (null nil) ; how to do nothing? :D
else if (eq (dom-tag elem) 'input)
do (push
(cons (dom-attr elem 'id)
`((:type . ,(dom-attr elem 'type))))
fields)
else if (eq (dom-tag elem) 'select)
do (push
(cons (dom-attr elem 'id)
`((:type . select)
(:options . ,(mapcar
(lambda (opt) (cons (dom-attr opt 'value)
(biome-api--fix-string
(dom-text opt))))
(dom-by-tag elem 'option)))))
fields)
else if (eq (dom-tag elem) 'label)
do (push (cons (dom-attr elem 'for)
(biome-api--fix-string (dom-text elem)))
field-names))
(cl-loop for (id . name) in field-names
do (when-let ((field (assoc id fields)))
(setf (cdr field) (cons `(:name . ,name) (cdr field)))))
(cl-loop for (id . replace-field) in biome-api--replace-variables
do (when-let ((field (assoc id fields)))
(setf (cdr field) (copy-tree (cdr replace-field)))))
(nreverse fields)))
(defun biome-api--parse-page-pills (section)
(when-let* ((pill-names
(mapcar
(lambda (button)
(cons (dom-attr button 'aria-controls)
(biome-api--fix-string (dom-text button))))
(dom-search
section
(lambda (el)
(and (eq (dom-tag el) 'button)
(string-match-p "nav-link" (dom-attr el 'class))))))))
(cl-loop for (id . name) in pill-names
collect `((:name . ,name)
(:fields
. ,(biome-api--parse-page-variables
;; XXX dom-by-id doesn't work here
(car (dom-search
section
(lambda (el)
(string= (dom-attr el 'id) id))))))))))
(defun biome-api--parse-page-accordion (section)
(when-let* ((accordion (car (dom-by-class section "accordion")))
(items (dom-by-class accordion "accordion-item")))
(let (res)
(cl-loop
for item in items
for item-name = (biome-api--fix-string
(dom-text (car (dom-by-class item "accordion-button"))))
if (dom-by-class item "nav-pills") do
(push `((:name . ,item-name)
(:children . ,(biome-api--parse-page-pills item)))
res)
else do (push `((:name . ,item-name)
(:fields . ,(biome-api--parse-page-variables item)))
res))
(nreverse res))))
(defun biome-api--parse-section (section)
(or (when-let ((accordion (biome-api--parse-page-accordion section)))
(list (cons :children accordion)))
(when-let ((variables (biome-api--parse-page-variables section)))
(list (cons :fields variables)))))
(defun biome-api--parse-postprocess-extract-models (res)
(let ((models
(seq-find
(lambda (item)
(string-match-p
"model" (downcase (cdr (assoc :name item)))))
res)))
(if models
(setq res (delete models res))
(setq res
(mapcar (lambda (item)
(when (assoc :children item)
(let ((extracted (biome-api--parse-postprocess-extract-models
(cadr (assoc :children item)))))
(setf (cadr (assoc :children item)) (car extracted))
(when (cdr extracted)
(setq models (cdr extracted)))))
item)
res)))
(cons res models)))
(defun biome-api--parse-postprocess (res)
(setq res (cl-loop for (name . data) in res
collect `((:name . ,name)
,@data)))
(when-let ((models-data (biome-api--parse-postprocess-extract-models res)))
(setq res (append (car models-data)
(list (cdr models-data)))))
res)
(defun biome-api--parse-page (html-string)
(let* ((html (with-temp-buffer
(insert html-string)
(libxml-parse-html-region (point-min) (point-max))))
(form (dom-by-id html "api_form"))
(endpoint (dom-attr form 'action))
(tree
(cl-loop with section-name = nil
with res = nil
for section in (dom-non-text-children form)
do (let ((h2s (dom-by-tag section 'h2)))
(when (and h2s (not (string-empty-p (dom-text (car h2s)))))
(setq section-name
(biome-api--fix-string
(dom-text (car h2s))))))
if (string= section-name "Usage License") return (nreverse res)
;; Merge different sections with the same name
unless (assoc section-name res)
do (push `(,section-name . nil) res)
do (let ((parsed-section (biome-api--parse-section section)))
(cl-loop for (kind . data) in parsed-section
unless (assoc kind (cdr (assoc section-name res)))
do (push `(,kind . nil) (cdr (assoc section-name res)))
do (setf (cdr (assoc kind (cdr (assoc section-name res))))
(cons data (cdr (assoc kind (cdr (assoc section-name res))))))))
finally return (nreverse res))))
(biome-api--parse-postprocess tree)))
;; (setq my/test2 (biome-api--parse-page my/test))
;; (request "https://open-meteo.com/en/docs"
;; :parser 'buffer-string
;; :success (cl-function
;; (lambda (&key data &allow-other-keys)
;; (setq my/test data))))
(provide 'biome-api)
;;; biome-api.el ends here