Heavily refactor esvapi.d (add ESVMode enum, ...) and apply changes to main.d

This commit is contained in:
Jeremy Baxter 2023-06-04 21:13:37 +12:00
parent 45890a6051
commit 8564fd3003
2 changed files with 117 additions and 90 deletions

159
esvapi.d
View file

@ -30,11 +30,18 @@ import std.format : format;
import std.json : JSONValue, parseJSON; import std.json : JSONValue, parseJSON;
import std.random : rndGen; import std.random : rndGen;
import std.range : take; import std.range : take;
import std.regex : matchAll, replaceAll, regex; import std.regex : matchAll, replaceAll, replaceFirst, regex;
import std.string : capitalize; import std.string : capitalize;
import std.utf : toUTF8; import std.utf : toUTF8;
import std.net.curl; import std.net.curl;
public enum ESVMode
{
TEXT,
AUDIO
}
const enum ESVAPI_KEY = "abfb7456fa52ec4292c79e435890cfa3df14dc2b";
const enum ESVAPI_URL = "https://api.esv.org/v3/passage"; const enum ESVAPI_URL = "https://api.esv.org/v3/passage";
const string[] BIBLE_BOOKS = [ const string[] BIBLE_BOOKS = [
// Old Testament // Old Testament
@ -56,8 +63,8 @@ const string[] BIBLE_BOOKS = [
"Nehemiah", "Nehemiah",
"Esther", "Esther",
"Job", "Job",
"Psalm", // <- "Psalm",
"Psalms", // <- both are valid "Psalms", // both are valid
"Proverbs", "Proverbs",
"Ecclesiastes", "Ecclesiastes",
"Song of Solomon", "Song of Solomon",
@ -110,54 +117,38 @@ const string[] BIBLE_BOOKS = [
class ESVApi class ESVApi
{ {
private string _key; private {
private string _url; int _mode;
private string _mode; string _key;
string _tmp;
string _url;
}
ESVApiOptions opts; ESVApiOptions opts;
string extraParameters; string extraParameters;
int delegate(size_t dlTotal, size_t dlNow, size_t ulTotal, size_t ulNow) onProgress; int delegate(size_t, size_t, size_t, size_t) onProgress;
string tmpDir; this(immutable(string) key = ESVAPI_KEY, bool audio = false)
this(immutable(string) key)
{ {
_url = ESVAPI_URL;
_key = key; _key = key;
_mode = "text"; _mode = audio ? ESVMode.AUDIO : ESVMode.TEXT;
opts.setDefaults(); _tmp = tempDir() ~ "esv";
_url = ESVAPI_URL;
opts.defaults();
extraParameters = ""; extraParameters = "";
onProgress = (size_t dlTotal, size_t dlNow, size_t ulTotal, size_t ulNow) {return 0;}; onProgress = (size_t dlTotal, size_t dlNow, size_t ulTotal, size_t ulNow) { return 0; };
tmpDir = tempDir() ~ "esvapi"; tmpName = "esv";
}
/*
* Returns the API URL currently in use.
*/
final string getURL() const nothrow @nogc @safe
{
return _url;
}
/*
* If the url argument is a valid HTTP URL, sets the API URL currently in use
* to the given url argument. Otherwise, throws an EsvException .
*/
final void setURL(immutable(string) url) @safe
{
auto matches = url.matchAll("^https?://.+\\..+(/.+)?");
if (matches.empty)
throw new EsvException("Invalid URL format");
else
_url = url;
} }
/* /*
* Returns the API authentication key that was given when the API object was instantiated. * Returns the API authentication key that was given when the API object was instantiated.
* This authentication key cannot be changed after instantiation. * This authentication key cannot be changed after instantiation.
*/ */
final string getKey() const nothrow @nogc @safe @nogc @property @safe string key() const nothrow
{ {
return _key; return _key;
} }
/* /*
* Returns the API authentication key currently in use. * Returns the API authentication key currently in use.
*/ */
final string getMode() const nothrow @nogc @safe @nogc @property @safe int mode() const nothrow
{ {
return _mode; return _mode;
} }
@ -165,27 +156,54 @@ class ESVApi
* If the mode argument is either "text" or "html", * If the mode argument is either "text" or "html",
* sets the text API mode to the given mode argument. * sets the text API mode to the given mode argument.
* If the mode argument is not one of those, * If the mode argument is not one of those,
* then this function will do nothing. * throws an ESVException.
*/ */
final void setMode(immutable(string) mode) nothrow @nogc @safe @property @safe void mode(immutable(int) mode)
{
foreach (string m; ["text", "html"] )
{
if (mode == m)
{ {
if (mode == ESVMode.TEXT || mode == ESVMode.AUDIO)
_mode = mode; _mode = mode;
return; else
throw new ESVException("Invalid mode");
} }
/*
* Returns the API URL currently in use.
*/
@nogc @property @safe string url() const nothrow
{
return _url;
} }
/*
* If the url argument is a valid HTTP URL, sets the API URL currently in use
* to the given url argument. Otherwise, throws an ESVException.
*/
@property @safe void url(immutable(string) url)
{
if (url.matchAll("^https?://.+\\..+(/.+)?").empty)
throw new ESVException("Invalid URL format");
else
_url = url;
}
/*
* Returns the temp directory name.
*/
@property @safe tmpName() const
{
return _tmp.replaceFirst(regex('^' ~ tempDir()), "");
}
/*
* Sets the temp directory name to the given string.
*/
@property @safe void tmpName(immutable(string) name)
{
_tmp = tempDir() ~ name;
} }
/* /*
* Returns true if the argument book is a valid book of the Bible. * Returns true if the argument book is a valid book of the Bible.
* Otherwise, returns false. * Otherwise, returns false.
*/ */
final bool validateBook(in char[] book) const nothrow @safe @safe bool validateBook(in char[] book) const nothrow
{
foreach (string b; BIBLE_BOOKS)
{ {
foreach (b; BIBLE_BOOKS) {
if (book.capitalize() == b.capitalize()) if (book.capitalize() == b.capitalize())
return true; return true;
} }
@ -195,42 +213,47 @@ class ESVApi
* Returns true if the argument book is a valid verse format. * Returns true if the argument book is a valid verse format.
* Otherwise, returns false. * Otherwise, returns false.
*/ */
final bool validateVerse(in char[] verse) const @safe @safe bool validateVerse(in char[] verse) const
{ {
bool attemptRegex(string re) const @safe @safe bool attemptRegex(string re) const
{ {
auto matches = verse.matchAll(re); return !verse.matchAll(re).empty;
return !matches.empty;
} }
if (attemptRegex("^\\d{1,3}$") || if (attemptRegex("^\\d{1,3}$") ||
attemptRegex("^\\d{1,3}-\\d{1,3}$") || attemptRegex("^\\d{1,3}-\\d{1,3}$") ||
attemptRegex("^\\d{1,3}:\\d{1,3}$") || attemptRegex("^\\d{1,3}:\\d{1,3}$") ||
attemptRegex("^\\d{1,3}:\\d{1,3}-\\d{1,3}$")) attemptRegex("^\\d{1,3}:\\d{1,3}-\\d{1,3}$"))
{
return true; return true;
}
else else
{
return false; return false;
} }
}
/* /*
* Requests the verse(s) from the API and returns it. * Requests the verse(s) from the API and returns it.
* The (case-insensitive) name of the book being searched are * The (case-insensitive) name of the book being searched are
* contained in the argument book. The verse(s) being looked up are * contained in the argument book. The verse(s) being looked up are
* contained in the argument verses. * contained in the argument verses.
* *
* If the mode is ESVMode.AUDIO, requests an audio passage instead.
* A file path to an MP3 audio track is returned.
* To explicitly get an audio passage without setting the mode,
* use getAudioVerses().
*
* Example: getVerses("John", "3:16-21") * Example: getVerses("John", "3:16-21")
*/ */
final string getVerses(in char[] book, in char[] verse) const string getVerses(in char[] book, in char[] verse) const
{ {
if (_mode == ESVMode.AUDIO) {
return getAudioVerses(book, verse);
}
if (!validateBook(book)) if (!validateBook(book))
throw new EsvException("Invalid book"); throw new ESVException("Invalid book");
if (!validateVerse(verse)) if (!validateVerse(verse))
throw new EsvException("Invalid verse format"); throw new ESVException("Invalid verse format");
string apiURL = format!"%s/%s/?q=%s+%s%s%s"(_url, _mode, string apiURL = format!"%s/%s/?q=%s+%s%s%s"(_url, _mode,
book.capitalize().replaceAll(regex(" "), "+"), verse, assembleParameters(), extraParameters); book.capitalize().replaceAll(regex(" "), "+"), verse,
assembleParameters(), extraParameters);
auto request = HTTP(apiURL); auto request = HTTP(apiURL);
string response; string response;
request.onProgress = onProgress; request.onProgress = onProgress;
@ -252,12 +275,12 @@ class ESVApi
* *
* Example: getVerses("John", "3:16-21") * Example: getVerses("John", "3:16-21")
*/ */
final string getAudioVerses(in char[] book, in char[] verse) const string getAudioVerses(in char[] book, in char[] verse) const
{ {
if (!validateBook(book)) if (!validateBook(book))
throw new EsvException("Invalid book"); throw new ESVException("Invalid book");
if (!validateVerse(verse)) if (!validateVerse(verse))
throw new EsvException("Invalid verse format"); throw new ESVException("Invalid verse format");
string apiURL = format!"%s/audio/?q=%s+%s"(_url, book.capitalize().replaceAll(regex(" "), "+"), verse); string apiURL = format!"%s/audio/?q=%s+%s"(_url, book.capitalize().replaceAll(regex(" "), "+"), verse);
auto request = HTTP(apiURL); auto request = HTTP(apiURL);
@ -274,7 +297,8 @@ class ESVApi
tmpFile.write(response); tmpFile.write(response);
return tmpFile; return tmpFile;
} }
private string assembleParameters() const @safe private:
@safe string assembleParameters() const
{ {
string params = ""; string params = "";
string addParam(string param, string value) const string addParam(string param, string value) const
@ -302,14 +326,13 @@ class ESVApi
params = addParam("indent-using", opts.indent_using.to!string); params = addParam("indent-using", opts.indent_using.to!string);
return params; return params;
} }
private string tempFile() const @safe @safe string tempFile() const
{ {
auto rndNums = rndGen().map!(a => cast(ubyte)a)().take(32); auto rndNums = rndGen().map!(a => cast(ubyte)a)().take(32);
auto result = appender!string(); auto result = appender!string();
Base64.encode(rndNums, result); Base64.encode(rndNums, result);
tmpDir.mkdirRecurse(); _tmp.mkdirRecurse();
string f = tmpDir ~ "/" ~ result.data.filter!isAlphaNum().to!string(); string f = _tmp ~ "/" ~ result.data.filter!isAlphaNum().to!string();
f.write("");
return f; return f;
} }
} }
@ -319,7 +342,7 @@ struct ESVApiOptions
bool[string] boolOpts; bool[string] boolOpts;
int[string] intOpts; int[string] intOpts;
string indent_using; string indent_using;
void setDefaults() nothrow @safe @safe void defaults() nothrow
{ {
boolOpts["include_passage_references"] = true; boolOpts["include_passage_references"] = true;
boolOpts["include_verse_numbers"] = true; boolOpts["include_verse_numbers"] = true;
@ -343,9 +366,9 @@ struct ESVApiOptions
} }
} }
class EsvException : Exception class ESVException : Exception
{ {
this(string msg, string file = __FILE__, size_t line = __LINE__) @safe pure @safe this(string msg, string file = __FILE__, size_t line = __LINE__) pure
{ {
super(msg, file, line); super(msg, file, line);
} }

22
main.d
View file

@ -33,7 +33,7 @@ import dini;
enum VERSION = "0.2.0"; enum VERSION = "0.2.0";
enum DEFAULT_APIKEY = "abfb7456fa52ec4292c79e435890cfa3df14dc2b"; // crossway approved ;) enum DEFAULT_APIKEY = "abfb7456fa52ec4292c79e435890cfa3df14dc2b";
enum DEFAULT_CONFIGPATH = "~/.config/esv.conf"; enum DEFAULT_CONFIGPATH = "~/.config/esv.conf";
enum DEFAULT_MPEGPLAYER = "mpg123"; enum DEFAULT_MPEGPLAYER = "mpg123";
enum DEFAULT_PAGER = "less"; enum DEFAULT_PAGER = "less";
@ -158,14 +158,15 @@ key = " ~ DEFAULT_APIKEY ~ "
panic(e.msg); panic(e.msg);
} }
string apiKey; string apiKey;
try apiKey = iniData["api"].getKey("key"); try
apiKey = iniData["api"].getKey("key");
catch (IniException e) catch (IniException e)
panic("API key not present in configuration file; cannot proceed"); panic("API key not present in configuration file; cannot proceed");
if (apiKey == "") if (apiKey == "")
panic("API key not present in configuration file; cannot proceed"); panic("API key not present in configuration file; cannot proceed");
// Initialise API object and validate the book and verse // Initialise API object and validate the book and verse
ESVApi esv = new ESVApi(apiKey); ESVApi esv = new ESVApi(apiKey, optAudio);
if (!esv.validateBook(args[1].extractBook())) if (!esv.validateBook(args[1].extractBook()))
panic("book '" ~ args[1] ~ "' does not exist"); panic("book '" ~ args[1] ~ "' does not exist");
if (!esv.validateVerse(args[2])) if (!esv.validateVerse(args[2]))
@ -179,10 +180,12 @@ key = " ~ DEFAULT_APIKEY ~ "
} else { } else {
string tmpf = esv.getAudioVerses(args[1], args[2]); string tmpf = esv.getAudioVerses(args[1], args[2]);
string mpegPlayer = environment.get(ENV_PLAYER, DEFAULT_MPEGPLAYER); string mpegPlayer = environment.get(ENV_PLAYER, DEFAULT_MPEGPLAYER);
// esv has built-in support for mpg123 and mpv /*
// other players will work, just recompile with * esv has built-in support for mpg123 and mpv;
// the DEFAULT_MPEGPLAYER enum set differently * other players will work, just recompile with
// or use the ESV_PLAYER environment variable * the DEFAULT_MPEGPLAYER enum set differently
* or use the ESV_PLAYER environment variable
*/
if (mpegPlayer == "mpg123") if (mpegPlayer == "mpg123")
mpegPlayer = mpegPlayer ~ " -q "; mpegPlayer = mpegPlayer ~ " -q ";
else if (mpegPlayer == "mpv") else if (mpegPlayer == "mpv")
@ -191,8 +194,8 @@ key = " ~ DEFAULT_APIKEY ~ "
mpegPlayer = DEFAULT_MPEGPLAYER ~ " "; mpegPlayer = DEFAULT_MPEGPLAYER ~ " ";
// spawn mpg123 // spawn mpg123
executeShell(mpegPlayer ~ tmpf); executeShell(mpegPlayer ~ tmpf);
return 0;
} }
return 0;
} }
esv.extraParameters = iniData["api"].getKey("parameters"); esv.extraParameters = iniData["api"].getKey("parameters");
@ -219,7 +222,8 @@ key = " ~ DEFAULT_APIKEY ~ "
} catch (IniException e) {} // just do nothing; use the default settings } catch (IniException e) {} // just do nothing; use the default settings
} }
// Get line_length ([passage]) // Get line_length ([passage])
try esv.opts.intOpts["line_length"] = returnValid("0", iniData["passage"].getKey("line_length")).to!int(); try
esv.opts.intOpts["line_length"] = returnValid("0", iniData["passage"].getKey("line_length")).to!int();
catch (ConvException e) { catch (ConvException e) {
panic(configPath ~ ": value '" ~ iniData["passage"].getKey("line_length") panic(configPath ~ ": value '" ~ iniData["passage"].getKey("line_length")
~ "' is not convertible to an integer value; must be a non-decimal number"); ~ "' is not convertible to an integer value; must be a non-decimal number");