Compare commits

...

42 Commits

Author SHA1 Message Date
ccccacd88d Makefile: Create install directories.
Sometimes these directories may not exist.
2024-11-04 16:52:57 +01:00
3157d98ea1 Update To-Do list. 2024-11-04 16:39:50 +01:00
3bb7f52823 Bump to version v1.0 2024-11-04 16:37:59 +01:00
93da965805 Add message for man page to help information. 2024-11-04 16:37:30 +01:00
4d07cdf4d4 Makefile: Add man file to distclean target. 2024-11-04 16:35:48 +01:00
edb5d29040 Add man page. 2024-11-02 11:44:50 +01:00
61136eced5 'Uncheck' v1.0
It didn't do what I thought it would do.
2024-11-01 15:56:39 +01:00
c6c3b45ec9 Use TODO for version roadmap. 2024-11-01 15:55:44 +01:00
6158aaf673 Fix README example. 2024-10-29 19:46:06 +01:00
49bb2f4fc8 Properly align columns from list subcommand. 2024-10-29 19:44:09 +01:00
4379b0311e Add safeguards to avoid bad usage. 2024-10-29 17:59:57 +01:00
a3b6471c13 Fix markdown paths for Gitea.
Apparently on Gitea it doesn't use `/` to mean project root like it does
on GitHub.
2024-10-21 18:49:58 +02:00
907f8a7b41 Add new TODO list item. 2024-10-21 18:45:00 +02:00
009e3f09bf Add feature to edit recipe description. 2024-10-21 18:44:38 +02:00
320346911c Add feature to edit name.
And documentation for editing the description.
2024-10-21 18:37:53 +02:00
0b348e8c10 Add usage information to README. 2024-10-21 14:58:48 +02:00
f68a8b45a5 Add new items to To-Do list. 2024-10-21 14:58:36 +02:00
feed35d8c8 Change database creation message. 2024-10-21 14:30:16 +02:00
db0b607546 Move position of 'list' subcommand.
It makes more sense in terms of ordering.
2024-10-21 14:22:13 +02:00
d8a140e913 Add Contributing section to README. 2024-10-21 14:17:34 +02:00
a1e3322576 Add To-Do list. 2024-10-21 14:11:06 +02:00
802f3bfcc0 Normalize use of const in db method parameters. 2024-10-21 14:05:26 +02:00
5873ceb627 Enable adding and removing tags from recipes. 2024-10-21 14:03:04 +02:00
5bcc598880 Enable adding and removing ingredients from recipes. 2024-10-21 13:54:07 +02:00
03b1250006 Add unique constraint in relational tables. 2024-10-12 11:02:56 +02:00
c71467804e Fix SQL constraint NOT NULL.
I had used sed to change everything at once, so naturally this forwent
my notice.
2024-10-12 10:57:47 +02:00
c31ccdece2 Close database upon errors. 2024-10-12 10:48:35 +02:00
d2f30dcca7 Rename db functions s/db_// 2024-10-12 10:41:35 +02:00
2434a516cd Remove unused ret variable in cmd_info(). 2024-10-12 10:27:59 +02:00
d30f8df5c1 Put database in class and use throw exceptions. 2024-10-11 17:43:28 +02:00
ba7f930231 Rename functions command_* -> cmd_* 2024-10-11 16:18:22 +02:00
e92a578d65 Add "i" alias to info command. 2024-10-11 16:15:00 +02:00
1a590b477e Add info command. 2024-10-11 12:01:22 +02:00
c0c6959774 Regularize token use.
Mainly using the 'not' and 'not_eq' tokens.
2024-10-11 10:54:14 +02:00
08b4834d9e Remove old comments from porting. 2024-10-11 10:52:11 +02:00
e25fb66dbc Add removal feature. 2024-10-11 10:43:38 +02:00
5f53180a60 Add list feature. 2024-10-11 09:24:48 +02:00
deb3f2d13f README: Fix specified C++ standard (c++20). 2024-10-10 07:30:28 +02:00
81eced2636 Finish porting to C++. 2024-10-10 07:27:22 +02:00
b5094d68e9 Port main.cpp to C++. 2024-10-09 20:14:42 +02:00
e86ca506c1 Port arg_parse.{cpp,hpp} to C++. 2024-10-09 20:14:41 +02:00
836daeb69b Switch to using C++. 2024-10-09 19:50:53 +02:00
18 changed files with 1325 additions and 505 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
/menu-helper
/compile_commands.json
.cache/*
/*.1.gz

View File

@ -18,9 +18,10 @@ DEBUG=0
INCFLAGS=
LDFLAGS=-lsqlite3
DEFS=
CFLAGS=$(INCFLAGS) -std=gnu99 -Wall -Wextra -Wfatal-errors -Werror
HDRS=src/arg_parse.h src/util.h src/db.h src/cmd.h
OBJS=src/main.o src/arg_parse.o src/db.o src/cmd.o
CFLAGS=$(INCFLAGS) -std=c++20 -Wall -Wextra -Wfatal-errors -Werror
HDRS=src/util.hpp src/arg_parse.hpp src/db.hpp src/cmd.hpp
OBJS=src/main.o src/util.o src/arg_parse.o src/db.o src/cmd.o
DOCS=menu-helper.1
VERSION=1.0
ifeq ($(PREFIX),)
@ -33,11 +34,14 @@ else
CFLAGS+=-O2 -DNDEBUG
endif
%.o:%.c $(HDRS)
$(CC) -c -o $@ $< $(CFLAGS) -DVERSION=\"$(VERSION)\"
%.o:%.cpp $(HDRS)
$(CXX) -c -o $@ $< $(CFLAGS) -DVERSION=\"$(VERSION)\"
menu-helper: $(OBJS)
$(CC) -o $@ $^ $(LDFLAGS)
$(CXX) -o $@ $^ $(LDFLAGS)
menu-helper.1.gz: $(DOCS)
gzip -c $< > $@
.PHONY: clean distclean install
@ -45,7 +49,11 @@ clean:
$(RM) $(OBJS)
distclean: clean
$(RM) menu-helper.1.gz
$(RM) menu-helper
install: menu-helper
install: menu-helper menu-helper.1.gz
install -d $(PREFIX)/bin
install -m 755 menu-helper $(PREFIX)/bin/
install -d $(PREFIX)/share/man/man1
install -m 644 menu-helper.1.gz $(PREFIX)/share/man/man1/

176
README.md
View File

@ -1,24 +1,188 @@
# Menu Helper
# Menu-Helper
A program to manage a database of recipes and help you to pick out meals based
on filters of ingredients and tags.
## Usage
Ensure the `XDG_DATA_HOME` variable is set (e.g. to `$HOME/.local/share`).
Ensure the `XDG_DATA_HOME` variable is set (e.g. to `$HOME/.local/share`) and
that you have installed the SQLite3 library.
Upon first execution of any command, the program will automatically create the
database.
### Adding New Recipes
The first thing you're probably going to want to do is to add a new recipe to
your database. If this database hasn't been created already then the program
will do it automatically. This is done via the `add` subcommand, which will
query you about the different attributes you want for your recipe, looking
something like the following:
```console
$ menu-helper add
Name: Linguine Scampi
Description: A lemony Italian pasta dish.
Ingredients (comma separated): linguine,shrimp,garlic,parsley,lemon
Tags (comma separated): italian,lunch
Creating database in /home/nicolas/.local/share/menu-helper/recipes.db
```
This will have created your recipe within the database. That last line there is
merely informative, telling you that the database did not exist and it is not
being created; if you had a database already and it isn't being found, ensure
that your `XDG_DATA_HOME` environment variable is properly set.
### Querying Recipes
#### Filtering
Once a recipe or two have been added to your database you may now query them
filtering based on ingredients and tags. This is done via the `list` command,
which takes two kinds of arguments, both optional:
- `-i <list>`: Comma-separated list of the ingredients to look for.
- `-t <list>`: Comma-separated list of the tags to look for.
If neither is specified then all recipes will be listed with their respective
ID, name, and description:
```console
$ menu-helper list
1 | Linguine Scampi | A lemony Italian pasta dish.
2 | Garlic Soup | A simple monastic soup for cold winters.
```
However, when one of these arguments is used it filters recipes to only show
those which include __all__ the ingredients and tags specified:
```console
$ menu-helper list -i linguine
1 | Linguine Scampi | A lemony Italian pasta dish.
```
#### Recipe Information
The IDs shown in the queries above now become useful for the rest of
Menu-Helper functionality. If you wish to query all stored information about a
given recipe, this is where you can use the `info` subcommand with the relevant
ID:
```console
$ menu-helper info 2
Name: Garlic Soup
Description: A simple monastic soup for cold winters.
ID: 2
Ingredients:
- garlic
- bread
- egg
Tags:
- soup
- dinner
- simple
```
### Removing Recipes
If you end up desiring to remove a recipe for whatever reason, you can do so by
using the `del` subcommand with the recipe's corresponding ID:
```console
$ menu-helper del 2
$ menu-helper list
1 | Linguine Scampi | A lemony Italian pasta dish.
```
### Modifying Recipes
#### Name & Description
To correct or otherwise modify the name or description of your recipe, you can
use the `edit-name` and `edit-description` subcommands. These will prompt you
for the new name or description respectively and overwrite what was previously
stored in the database:
```console
$ menu-helper edit-name 1
New name: Linguine agli Scampi
$ menu-helper edit-description 1
New description: A zesty Italian pasta dish.
$ menu-helper list
1 | Linguine agli Scampi | A zesty Italian pasta dish.
```
#### Ingredients/Tags
If there are ingredients/tags which you forgot to add to a recipe, or that you
added erringly, you can correct this with the following commands:
- `add-ingr <id> <list>`: Add list of comma-separated ingredients `list` to
recipe with ID `id`.
- `rm-ingr <id> <list>`: Remove list of comma-separated ingredients `list` from
recipe with ID `id`.
- `add-tag <id> <list>`: Add list of comma-separated tags `list` to recipe with
ID `id`.
- `rm-tag <id> <list>`: Remove list of comma-separated tags `list` from recipe
with ID `id`.
For example, we forgot to add the useful tag to our first recipe (Linguine
Scampi) that it is a pasta dish. We can do this with the following command:
```console
$ menu-helper add-tag 1 pasta
$ menu-helper info 1
Name: Linguine agli Scampi
Description: A zesty Italian pasta dish.
ID: 1
Ingredients:
- linguine
- shrimp
- garlic
- parsley
- lemon
Tags:
- italian
- lunch
- pasta
```
## Building
To build the program you will require the following dependencies:
- A C compiler compatible with GNU99 C (preferably GCC).
- SQLite3 C library
- A C++ compiler compatible with C++20 (preferably GCC).
- SQLite3 C/C++ library
- Make
Once installed, compile the project with the `make` command.
Once installed, compile the project with the `make` command. To install simply
run the `make install` command, optionally appending `PREFIX=...` to change the
default directory of installation (i.e. `/usr/local/...`).
## Contributing
If you find any issues, feel free to report them on GitHub or send me an E-Mail
(see my website/profile for the address). I will add these issues to my personal
Gitea page and (unless specified otherwise) mention you as the person who found
the issue.
For patches/pull requests, if you open a PR on GitHub I will likely not merge
directly but instead apply the patches locally (via Git patches, conserving
authorship), push them to my Gitea repository, which will finally be mirrored to
GitHub. However, you can save me a bit of work by just sending me the Git
patches directly (via E-Mail).
If you're looking for a way to contribute, consider having a look at my [To-Do
list](TODO.md) for the project.
## License
This program is licensed under the terms & conditions of the GNU General Public
License version 3 or greater. See the [LICENSE](/LICENSE) file for more
License version 3 or greater. See the [LICENSE](LICENSE) file for more
information.

16
TODO.md Normal file
View File

@ -0,0 +1,16 @@
# To-Do List
- [X] v1.0
- [X] Add basic functionality.
- [X] Add more safeguards to avoid bad usage.
- [X] Create a man page.
- [X] Add more documentation to `help` subcommand.
- [X] Properly align output columns from `list` subcommand.
- [X] Add feature for editing recipe name and description.
- [X] Name
- [X] Description
- [ ] v1.1
- [ ] Add import/export functionality.
- [ ] Allow for writing description in editor.
- [ ] Add examples to man page.

65
menu-helper.1 Normal file
View File

@ -0,0 +1,65 @@
.TH "MENU HELPER" "1" "November 2024" "menu-helper 1.0" "User Commands"
.SH "NAME"
menu-helper \- makes choosing meals easier
.SH "SYNOPSIS"
.B menu-helper
<\fICOMMAND\fR> [\fIOPTIONS\fR]
.SH "DESCRIPTION"
A program to manage a database of recipes and help you pick out meals based on
filters of ingredients and tags.
.SH "COMMANDS"
.TP
.B \fBadd\fR, \fBnew\fR
Add a new recipe to the database.
.TP
.B \fBdel\fR, \fBrm\fR <\fIid\fR>
Delete recipe with provided \fIid\fR.
.TP
.B \fBlist\fR, \fBls\fR [-i <\fIingredients\fR>] [-t <\fItags\fR>]
List all recipes that contain all \fIingredients\fR an \fItags\fR listed. If
none are listed, then it prints all recipes stored in the database. Both
\fIingredients\fR and \fItags\fR are comma-separated lists (e.g.
"garlic,tomato").
.TP
.B \fBinfo\fR <\fIid\fR>
Show all stored information on recipe with provided \fIid\fR.
.TP
.B \fBedit-name\fR <\fIid\fR>
Change the name of the recipe with the provided \fIid\fR.
.TP
.B \fBedit-description\fR, \fBedit-desc\fR <\fIid\fR>
Change the description of the recipe with the provided \fIid\fR.
.TP
.B \fBadd-ingr\fR <\fIid\fR> <\fIingredients\fR>
Add the specified \fIingredients\fR to the recipe with \fIid\fR, where
\fIingredients\fR is a comma-separated list (e.g. "garlic,tomato").
.TP
.B \fBrm-ingr\fR <\fIid\fR> <\fIingredients\fR>
Remove the specified \fIingredients\fR from the recipe with \fIid\fR, where
\fIingredients\fR is a comma-separated list (e.g. "garlic,tomato").
.TP
.B \fBadd-tag\fR <\fIid\fR> <\fItags\fR>
Add the specified \fItags\fR to the recipe with \fIid\fR, where \fItags\fR is a
comma-separated list (e.g. "dinner,simple").
.TP
.B \fBrm-tag\fR <\fIid\fR> <\fItags\fR>
Remove the specified \fItags\fR from the recipe with \fIid\fR, where \fItags\fR
is a comma-separated list (e.g. "dinner,simple").
.TP
.B \fBhelp\fR, \fB-h\fR, \fB--help\fR
Show basic help information.
.TP
.B \fBversion\fR, \fB-v\fR, \fB--version\fR
Show version information.
.SH "AUTHOR"
Written by Nicolás A. Ortega Froysa.
.SH "COPYRIGHT"
Copyright \(co 2024 Ortega Froysa, Nicolás A. <nicolas@ortegas.org>.
License: GNU General Public License version 3 or greater (see <https://gnu.org/licenses/gpl.html>).
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

View File

@ -15,6 +15,15 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "arg_parse.hpp"
int command_add(void);
enum cmd_id parse_args(const std::string &cmd) {
for(const auto &command : commands) {
for(const auto &alias : command.second) {
if(cmd == alias)
return command.first;
}
}
return CMD_UNKNOWN;
}

View File

@ -1,60 +0,0 @@
/*
* Copyright (C) 2024 Nicolás Ortega Froysa <nicolas@ortegas.org>
* Nicolás Ortega Froysa <nicolas@ortegas.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <stdio.h>
enum cmd_id {
CMD_UNKNOWN = 0,
CMD_ADD,
CMD_HELP,
CMD_VERSION,
};
struct cmd {
enum cmd_id id;
const char *str[3];
};
static const struct cmd commands[] = {
{ CMD_ADD, {"add", "new"} },
{ CMD_HELP, {"help", "-h", "--help"} },
{ CMD_VERSION, {"version", "-v", "--version"} },
};
static inline void print_version(void) {
printf("menu-helper v%s\n\n", VERSION);
}
static inline void print_usage(void) {
printf("USAGE: menu-helper <cmd> [options]\n\n");
}
static inline void print_help(void) {
print_version();
print_usage();
printf("COMMANDS:\n"
"\tadd, new Add a new recipe to the database\n"
"\thelp, -h, --help Show this help information.\n"
"\tversion, -v, --version Show version information.\n"
"\n");
}
enum cmd_id parse_args(const char *cmd);

85
src/arg_parse.hpp Normal file
View File

@ -0,0 +1,85 @@
/*
* Copyright (C) 2024 Nicolás Ortega Froysa <nicolas@ortegas.org>
* Nicolás Ortega Froysa <nicolas@ortegas.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <iostream>
#include <string>
#include <map>
#include <vector>
enum cmd_id {
CMD_UNKNOWN = 0,
CMD_ADD,
CMD_DEL,
CMD_LIST,
CMD_INFO,
CMD_EDIT_NAME,
CMD_EDIT_DESC,
CMD_ADD_INGR,
CMD_RM_INGR,
CMD_ADD_TAG,
CMD_RM_TAG,
CMD_HELP,
CMD_VERSION,
};
static const std::map<enum cmd_id, std::vector<std::string>> commands = {
{ CMD_ADD, {"add", "new"} },
{ CMD_DEL, {"del", "rm"} },
{ CMD_LIST, {"list", "ls"} },
{ CMD_INFO, {"info", "i"} },
{ CMD_EDIT_NAME, {"edit-name"} },
{ CMD_EDIT_DESC, {"edit-description", "edit-desc"} },
{ CMD_ADD_INGR, {"add-ingr"} },
{ CMD_RM_INGR, {"rm-ingr"} },
{ CMD_ADD_TAG, {"add-tag"} },
{ CMD_RM_TAG, {"rm-tag"} },
{ CMD_HELP, {"help", "-h", "--help"} },
{ CMD_VERSION, {"version", "-v", "--version"} },
};
static inline void print_version(void) {
std::cout << "menu-helper v" << VERSION << "\n" << std::endl;
}
static inline void print_usage(void) {
std::cout << "USAGE: menu-helper <cmd> [options]\n" << std::endl;
}
static inline void print_help(void) {
print_version();
print_usage();
std::cout << "COMMANDS:\n"
"\tadd, new Add a new recipe to the database.\n"
"\tdel, rm Delete recipe by ID.\n"
"\tlist, ls List recipes with filters.\n"
"\tinfo Show recipe information.\n"
"\tedit-name Change recipe name.\n"
"\tedit-description, edit-desc Change recipe description.\n"
"\tadd-ingr Add ingredient to a recipe.\n"
"\trm-ingr Remove ingredient from a recipe.\n"
"\tadd-tag Add tag to a recipe.\n"
"\trm-tag Remove tag from a recipe.\n"
"\thelp, -h, --help Show this help information.\n"
"\tversion, -v, --version Show version information.\n"
<< std::endl;
std::cout << "For more information about subcommands, use 'man menu-helper'." << std::endl;
}
enum cmd_id parse_args(const std::string &cmd);

100
src/cmd.c
View File

@ -1,100 +0,0 @@
/*
* Copyright (C) 2024 Nicolás Ortega Froysa <nicolas@ortegas.org>
* Nicolás Ortega Froysa <nicolas@ortegas.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "cmd.h"
#include "db.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
int command_add(void) {
char *name = NULL, *description = NULL, *ingredients = NULL, *tags = NULL;
size_t name_len, description_len, ingredients_len, tags_len;
int recipe_id, ingredient_id, tag_id;
if(!db_open()) {
fprintf(stderr, "Failed to open database. Cannot add new entry.\n");
return 0;
}
printf("Name: ");
getline(&name, &name_len, stdin);
// eliminate trailing newline
name[strlen(name) - 1] = '\0';
printf("Description: ");
getline(&description, &description_len, stdin);
// eliminate trailing newline
description[strlen(description) - 1] = '\0';
printf("Ingredients (comma separated): ");
getline(&ingredients, &ingredients_len, stdin);
// eliminate trailing newline
ingredients[strlen(ingredients) - 1] = '\0';
printf("Tags (comma separated): ");
getline(&tags, &tags_len, stdin);
// eliminate trailing newline
tags[strlen(tags) - 1] = '\0';
if((recipe_id = db_get_recipe_id(name)) <= 0)
recipe_id = db_add_recipe(name, description);
free(name);
free(description);
for(char *i = strtok(ingredients, ","); i; i = strtok(NULL,",")) {
// remove leading blank spaces
while(isblank(i[0]))
i += sizeof(char);
// remove trailing blank spaces
size_t i_len = strlen(i);
while(isblank(i[i_len - 1])) {
i[i_len - 1] = '\0';
--i_len;
}
if((ingredient_id = db_get_ingredient_id(i)) <= 0)
ingredient_id = db_add_ingredient(i);
db_conn_recipe_ingredient(recipe_id, ingredient_id);
}
free(ingredients);
for(char *i = strtok(tags, ","); i; i = strtok(NULL, ",")) {
// remove leading blank spaces
while(isblank(i[0]))
i += sizeof(char);
// remove trailing blank spaces
size_t i_len = strlen(i);
while(isblank(i[i_len - 1])) {
i[i_len - 1] = '\0';
--i_len;
}
if((tag_id = db_get_tag_id(i)) <= 0)
tag_id = db_add_tag(i);
db_conn_recipe_tag(recipe_id, tag_id);
}
free(tags);
db_close();
return 1;
}

339
src/cmd.cpp Normal file
View File

@ -0,0 +1,339 @@
/*
* Copyright (C) 2024 Nicolás Ortega Froysa <nicolas@ortegas.org>
* Nicolás Ortega Froysa <nicolas@ortegas.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "cmd.hpp"
#include "db.hpp"
#include "util.hpp"
#include <cstdlib>
#include <sys/ioctl.h>
#include <iomanip>
#include <iostream>
#include <string>
#include <unistd.h>
#include <vector>
int cmd_add(void) {
db db;
std::string name, description, ingredients, tags;
int recipe_id, ingredient_id, tag_id;
std::cout << "Name: ";
getline(std::cin, name);
std::cout << "Description: ";
getline(std::cin, description);
std::cout << "Ingredients (comma separated): ";
getline(std::cin, ingredients);
std::cout << "Tags (comma separated): ";
getline(std::cin, tags);
db.open();
if((recipe_id = db.get_recipe_id(name)) <= 0)
recipe_id = db.add_recipe(name, description);
for(auto &ingredient : split(ingredients, ",")) {
trim(ingredient);
if((ingredient_id = db.get_ingredient_id(ingredient)) <= 0)
ingredient_id = db.add_ingredient(ingredient);
db.conn_recipe_ingredient(recipe_id, ingredient_id);
}
for(auto &tag : split(tags, ",")) {
trim(tag);
if((tag_id = db.get_tag_id(tag)) <= 0)
tag_id = db.add_tag(tag);
db.conn_recipe_tag(recipe_id, tag_id);
}
db.close();
return EXIT_SUCCESS;
}
int cmd_list(int argc, char *argv[]) {
db db;
std::vector<std::string> ingredients, tags;
struct winsize winsize;
const int id_col_sz = 5, name_col_sz = 24;
int opt;
while((opt = getopt(argc, argv, "i:t:")) not_eq -1) {
switch(opt) {
case 'i':
ingredients = split(optarg, ",");
for(auto &i : ingredients)
trim(i);
break;
case 't':
tags = split(optarg, ",");
for(auto &i : tags)
trim(i);
break;
case '?':
std::cerr << "Unknown option '" << static_cast<char>(optopt)
<< "'. Use 'help' for information." << std::endl;
return EXIT_FAILURE;
}
}
ioctl(STDOUT_FILENO, TIOCGWINSZ, &winsize);
const int desc_col_sz = winsize.ws_col - (id_col_sz + name_col_sz + 1);
db.open();
std::cout << std::left << std::setw(id_col_sz) << "ID"
<< std::setw(name_col_sz) << "NAME"
<< std::setw(desc_col_sz) << "DESCRIPTION" << std::endl;
for(const auto &recipe : db.get_recipes(ingredients, tags)) {
std::cout << std::left << std::setw(id_col_sz) << recipe.id
<< std::setw(name_col_sz) << recipe.name
<< std::setw(desc_col_sz) << recipe.description << std::endl;
}
db.close();
return EXIT_SUCCESS;
}
int cmd_delete(int argc, char *argv[]) {
db db;
std::vector<int> recipe_ids;
if(argc < 1) {
std::cerr << "No specified IDs. Use 'help' for more information." << std::endl;
return EXIT_FAILURE;
}
db.open();
for(int i = 0; i < argc; ++i) {
const int id = std::stoi(argv[i]);
if(not db.recipe_exists(id)) {
std::cerr << "No recipe exists with ID " << id << "." << std::endl;
db.close();
return EXIT_FAILURE;
} else {
recipe_ids.push_back(id);
}
}
db.del_recipes(recipe_ids);
db.close();
return EXIT_SUCCESS;
}
int cmd_info(const int id) {
db db;
struct recipe recipe;
std::vector<std::string> ingredients, tags;
db.open();
if(not db.recipe_exists(id)) {
std::cerr << "No recipe with ID '" << id << "'";
db.close();
return EXIT_FAILURE;
}
recipe = db.get_recipe(id);
ingredients = db.get_recipe_ingredients(id);
tags = db.get_recipe_tags(id);
db.close();
std::cout << "Name: " << recipe.name << "\n"
<< "Description: " << recipe.description << "\n"
<< "ID: " << recipe.id << "\n"
<< std::endl;
std::cout << "Ingredients:" << std::endl;
for(auto &ingredient : ingredients)
std::cout << "\t- " << ingredient << std::endl;
std::cout << std::endl;
std::cout << "Tags:" << std::endl;
for(auto &tag : tags)
std::cout << "\t- " << tag << std::endl;
std::cout << std::endl;
return EXIT_SUCCESS;
}
int cmd_edit_name(const int id) {
db db;
std::string new_name;
db.open();
if(not db.recipe_exists(id)) {
std::cerr << "Recipe with ID " << id << " does not exist." << std::endl;
db.close();
return EXIT_FAILURE;
}
std::cout << "New name: ";
std::getline(std::cin, new_name);
db.update_recipe_name(id, new_name);
db.close();
return EXIT_SUCCESS;
}
int cmd_edit_desc(const int id) {
db db;
std::string new_desc;
db.open();
if(not db.recipe_exists(id)) {
std::cerr << "Recipe with ID " << id << " does not exist." << std::endl;
db.close();
return EXIT_FAILURE;
}
std::cout << "New name: ";
std::getline(std::cin, new_desc);
db.update_recipe_desc(id, new_desc);
db.close();
return EXIT_SUCCESS;
}
int cmd_add_ingr(const int recipe_id, const char *ingredients) {
db db;
std::vector<std::string> ingr_list = split(ingredients, ",");
db.open();
if(not db.recipe_exists(recipe_id)) {
std::cerr << "Recipe with ID " << recipe_id << " does not exist." << std::endl;
db.close();
return EXIT_FAILURE;
}
for(auto &i : ingr_list) {
int ingr_id;
trim(i);
if(not db.ingredient_exists(i))
ingr_id = db.add_ingredient(i);
else
ingr_id = db.get_ingredient_id(i);
db.conn_recipe_ingredient(recipe_id, ingr_id);
}
db.close();
return EXIT_SUCCESS;
}
int cmd_rm_ingr(const int recipe_id, const char *ingredients) {
db db;
std::vector<std::string> ingr_list = split(ingredients, ",");
db.open();
if(not db.recipe_exists(recipe_id)) {
std::cerr << "Recipe with ID " << recipe_id << " does not exist." << std::endl;
db.close();
return EXIT_FAILURE;
}
for(auto &i : ingr_list) {
int ingr_id;
trim(i);
if(not db.ingredient_exists(i)) {
std::cerr << "Could not find ingredient '" << i << "'. Skipping!" << std::endl;
continue;
}
ingr_id = db.get_ingredient_id(i);
db.disconn_recipe_ingredient(recipe_id, ingr_id);
}
db.close();
return EXIT_SUCCESS;
}
int cmd_add_tag(const int recipe_id, const char *tags) {
db db;
std::vector<std::string> tag_list = split(tags, ",");
db.open();
if(not db.recipe_exists(recipe_id)) {
std::cerr << "Recipe with ID " << recipe_id << " does not exist." << std::endl;
db.close();
return EXIT_FAILURE;
}
for(auto &i : tag_list) {
int tag_id;
trim(i);
if(not db.tag_exists(i))
tag_id = db.add_tag(i);
else
tag_id = db.get_ingredient_id(i);
db.conn_recipe_tag(recipe_id, tag_id);
}
db.close();
return EXIT_SUCCESS;
}
int cmd_rm_tag(const int recipe_id, const char *tags) {
db db;
std::vector<std::string> tag_list = split(tags, ",");
db.open();
if(not db.recipe_exists(recipe_id)) {
std::cerr << "Recipe with ID " << recipe_id << " does not exist." << std::endl;
db.close();
return EXIT_FAILURE;
}
for(auto &i : tag_list) {
int tag_id;
trim(i);
if(not db.tag_exists(i)) {
std::cerr << "Could not find tag '" << i << "'. Skipping!" << std::endl;
continue;
}
tag_id = db.get_tag_id(i);
db.disconn_recipe_tag(recipe_id, tag_id);
}
db.close();
return EXIT_SUCCESS;
}

View File

@ -15,18 +15,15 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "arg_parse.h"
#pragma once
#include "util.h"
#include <string.h>
enum cmd_id parse_args(const char *cmd) {
for(int i = 0; i < (int)ARRAY_LEN(commands); ++i) {
for(int j = 0; j < (int)ARRAY_LEN(commands[i].str); ++j) {
if(commands[i].str[j] && strcmp(commands[i].str[j], cmd) == 0)
return commands[i].id;
}
}
return CMD_UNKNOWN;
}
int cmd_add(void);
int cmd_list(int argc, char *argv[]);
int cmd_delete(int argc, char *argv[]);
int cmd_info(const int id);
int cmd_edit_name(const int id);
int cmd_edit_desc(const int id);
int cmd_add_ingr(const int recipe_id, const char *ingredients);
int cmd_rm_ingr(const int recipe_id, const char *ingredients);
int cmd_add_tag(const int recipe_id, const char *tags);
int cmd_rm_tag(const int recipe_id, const char *tags);

221
src/db.c
View File

@ -1,221 +0,0 @@
/*
* Copyright (C) 2024 Nicolás Ortega Froysa <nicolas@ortegas.org>
* Nicolás Ortega Froysa <nicolas@ortegas.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "db.h"
#include <sqlite3.h>
#include <unistd.h>
#include <sys/stat.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
static sqlite3 *db = NULL;
static const int db_version = 1;
int db_open(void) {
const char *xdg_data_home;
char *db_path;
int new_db = 0;
int rc;
if(!(xdg_data_home = getenv("XDG_DATA_HOME"))) {
printf("Cannot find environment variable XDG_DATA_HOME. Please define it before continuing. E.g.:\nexport XDG_DATA_HOME=\"$HOME/.local/share\"\n");
return 0;
}
db_path = malloc(strlen(xdg_data_home) + strlen("/menu-helper") + strlen("/recipes.db") + 1);
strcpy(db_path, xdg_data_home);
strcat(db_path, "/menu-helper");
if(access(db_path, F_OK) != 0)
mkdir(db_path, 0700);
strcat(db_path, "/recipes.db");
if(access(db_path, F_OK) != 0) {
printf("Creating database: %s\n", db_path);
new_db = 1;
}
rc = sqlite3_open(db_path, &db);
free(db_path);
if(rc == SQLITE_OK && new_db) {
char insert_version_stmt[64];
sqlite3_exec(db, "CREATE TABLE db_version(version INTEGER UNIQUE NOT NULL);", NULL, NULL, NULL);
snprintf(insert_version_stmt, 64, "INSERT INTO db_version VALUES(%d);", db_version);
sqlite3_exec(db, insert_version_stmt, NULL, NULL, NULL);
sqlite3_exec(db, "CREATE TABLE tags(id INTEGER PRIMARY KEY AUTOINCREMENT, name STRING UNIQUE);", NULL, NULL, NULL);
sqlite3_exec(db, "CREATE TABLE ingredients(id INTEGER PRIMARY KEY AUTOINCREMENT, name STRING UNIQUE);", NULL, NULL, NULL);
sqlite3_exec(db, "CREATE TABLE recipes(id INTEGER PRIMARY KEY AUTOINCREMENT, name STRING UNIQUE, description STRING);", NULL, NULL, NULL);
sqlite3_exec(db, "CREATE TABLE recipe_tag(recipe_id INTEGER REFERENCES recipes(id) ON DELETE CASCADE, tag_id INTEGER REFERENCES tags(id) ON DELETE CASCADE);", NULL, NULL, NULL);
sqlite3_exec(db, "CREATE TABLE recipe_ingredient(recipe_id INTEGER REFERENCES recipes(id) ON DELETE CASCADE, ingredient_id INTEGER REFERENCES ingredients(id) ON DELETE CASCADE);", NULL, NULL, NULL);
}
return rc == SQLITE_OK;
}
void db_close(void) {
if(!db)
return;
sqlite3_close(db);
}
int query_id_cb(void *recipe_id_var, int col_num, char **col_data, char **col_name) {
int *recipe_id_ptr = (int*)recipe_id_var;
int ret = 1;
for(int i = 0; i < col_num; ++i) {
if(strcmp(col_name[i], "id") == 0) {
*recipe_id_ptr = atoi(col_data[i]);
ret = 0;
break;
}
}
return ret;
}
int table_get_id_by_name(const char *table, const char *name) {
const char *sel_query_fmt = "SELECT id FROM %s WHERE lower(name)=lower('%s');";
char *sel_query;
int id = 0;
sel_query = malloc(strlen(table) + strlen(name) + strlen(sel_query_fmt) + 1);
sprintf(sel_query, sel_query_fmt, table, name);
if(sqlite3_exec(db, sel_query, query_id_cb, &id, NULL) != SQLITE_OK) {
free(sel_query);
return -2;
}
free(sel_query);
return id;
}
int db_add_recipe(const char *name, const char *description) {
const char *add_query_fmt = "INSERT INTO recipes(name,description) VALUES('%s','%s');";
char *add_query;
if(!db)
return -1;
add_query = malloc(strlen(name) + strlen(description) + strlen(add_query_fmt) + 1);
sprintf(add_query, add_query_fmt, name, description);
if(sqlite3_exec(db, add_query, NULL, NULL, NULL) != SQLITE_OK) {
free(add_query);
return -2;
}
free(add_query);
return db_get_recipe_id(name);
}
int db_get_recipe_id(const char *name) {
if(!db)
return -1;
return table_get_id_by_name("recipes", name);
}
int db_add_ingredient(const char *name) {
const char *add_query_fmt = "INSERT INTO ingredients(name) VALUES(lower('%s'));";
char *add_query;
if(!db)
return -1;
add_query = malloc(strlen(name) + strlen(add_query_fmt) + 1);
sprintf(add_query, add_query_fmt, name);
if(sqlite3_exec(db, add_query, NULL, NULL, NULL) != SQLITE_OK) {
free(add_query);
return -2;
}
free(add_query);
return db_get_ingredient_id(name);
}
int db_get_ingredient_id(const char *name) {
if(!db)
return -1;
return table_get_id_by_name("ingredients", name);
}
int db_add_tag(const char *name) {
const char *add_query_fmt = "INSERT INTO tags(name) VALUES('%s');";
char *add_query;
if(!db)
return -1;
add_query = malloc(strlen(name) + strlen(add_query_fmt) + 1);
sprintf(add_query, add_query_fmt, name);
if(sqlite3_exec(db, add_query, NULL, NULL, NULL) != SQLITE_OK) {
free(add_query);
return -2;
}
free(add_query);
return db_get_tag_id(name);
}
int db_get_tag_id(const char *name) {
if(!db)
return -1;
return table_get_id_by_name("tags", name);
}
int db_conn_recipe_ingredient(int recipe_id, int ingredient_id) {
const char *add_conn_fmt = "INSERT INTO recipe_ingredient(recipe_id, ingredient_id) VALUES(%d,%d);";
char *add_conn_query;
if(!db)
return -1;
add_conn_query = malloc(strlen(add_conn_fmt) + (recipe_id % 10) + (ingredient_id % 10));
sprintf(add_conn_query, add_conn_fmt, recipe_id, ingredient_id);
if(sqlite3_exec(db, add_conn_query, NULL, NULL, NULL) != SQLITE_OK) {
free(add_conn_query);
return -2;
}
free(add_conn_query);
return 1;
}
int db_conn_recipe_tag(int recipe_id, int tag_id) {
const char *add_conn_fmt = "INSERT INTO recipe_tag(recipe_id, tag_id) VALUES(%d,%d);";
char *add_conn_query;
if(!db)
return -1;
add_conn_query = malloc(strlen(add_conn_fmt) + (recipe_id % 10) + (tag_id % 10));
sprintf(add_conn_query, add_conn_fmt, recipe_id, tag_id);
if(sqlite3_exec(db, add_conn_query, NULL, NULL, NULL) != SQLITE_OK) {
free(add_conn_query);
return -2;
}
free(add_conn_query);
return 1;
}

368
src/db.cpp Normal file
View File

@ -0,0 +1,368 @@
/*
* Copyright (C) 2024 Nicolás Ortega Froysa <nicolas@ortegas.org>
* Nicolás Ortega Froysa <nicolas@ortegas.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "db.hpp"
#include <cstdlib>
#include <filesystem>
#include <format>
#include <iostream>
#include <sqlite3.h>
#include <stdexcept>
#define DB_VERSION 1
void db::open(void) {
std::string xdg_data_home;
std::string db_path;
bool new_db = false;
if((xdg_data_home = std::getenv("XDG_DATA_HOME")).empty())
throw std::runtime_error("Cannot find environment variable XDG_DATA_HOME. Please define it before continuing.");
db_path = xdg_data_home + "/menu-helper";
if(not std::filesystem::exists(db_path))
std::filesystem::create_directories(db_path);
db_path += "/recipes.db";
if(not std::filesystem::exists(db_path)) {
std::cout << "Creating database in " << db_path << std::endl;
new_db = true;
}
if(sqlite3_open(db_path.c_str(), &sqlite_db) not_eq SQLITE_OK)
throw std::runtime_error("Failed to open database file " + db_path);
if(new_db) {
sqlite3_exec(sqlite_db, "CREATE TABLE db_version(version INTEGER UNIQUE NOT NULL);", nullptr, nullptr, nullptr);
sqlite3_exec(sqlite_db, std::format("INSERT INTO db_version VALUES({});", DB_VERSION).c_str(), nullptr, nullptr, nullptr);
sqlite3_exec(sqlite_db, "CREATE TABLE tags(id INTEGER PRIMARY KEY AUTOINCREMENT, name STRING UNIQUE);", nullptr, nullptr, nullptr);
sqlite3_exec(sqlite_db, "CREATE TABLE ingredients(id INTEGER PRIMARY KEY AUTOINCREMENT, name STRING UNIQUE);", nullptr, nullptr, nullptr);
sqlite3_exec(sqlite_db, "CREATE TABLE recipes(id INTEGER PRIMARY KEY AUTOINCREMENT, name STRING UNIQUE, description STRING);", nullptr, nullptr, nullptr);
sqlite3_exec(sqlite_db, "CREATE TABLE recipe_tag(recipe_id INTEGER REFERENCES recipes(id) ON DELETE CASCADE, tag_id INTEGER REFERENCES tags(id) ON DELETE CASCADE, UNIQUE(recipe_id, tag_id));", nullptr, nullptr, nullptr);
sqlite3_exec(sqlite_db, "CREATE TABLE recipe_ingredient(recipe_id INTEGER REFERENCES recipes(id) ON DELETE CASCADE, ingredient_id INTEGER REFERENCES ingredients(id) ON DELETE CASCADE, UNIQUE(recipe_id, ingredient_id));", nullptr, nullptr, nullptr);
}
}
void db::close(void) {
if(not sqlite_db)
return;
sqlite3_close(sqlite_db);
sqlite_db = nullptr;
}
int query_id_cb(void *recipe_id_var, int col_num, char **col_data, char **col_name) {
int *recipe_id_ptr = (int*)recipe_id_var;
int ret = 1;
for(int i = 0; i < col_num; ++i) {
if(std::string(col_name[i]) == "id") {
*recipe_id_ptr = std::atoi(col_data[i]);
ret = 0;
break;
}
}
return ret;
}
int db::table_get_id_by_name(const std::string &table, const std::string &name) {
int id = 0;
if(not sqlite_db)
throw std::runtime_error(std::format("{}: Database not open! Please contact a developer.", __PRETTY_FUNCTION__));
if(sqlite3_exec(sqlite_db, std::format("SELECT id FROM {} WHERE lower(name)=lower('{}');", table, name).c_str(),
query_id_cb, &id, nullptr) not_eq SQLITE_OK) {
throw std::runtime_error(std::format("Failed to get ID of '{}' from table '{}'.", name, table));
}
return id;
}
int db::add_recipe(const std::string &name, const std::string &description) {
if(not sqlite_db)
throw std::runtime_error(std::format("{}: Database not open! Please contact a developer.", __PRETTY_FUNCTION__));
if(sqlite3_exec(sqlite_db, std::format("INSERT OR IGNORE INTO recipes(name,description) VALUES('{}','{}');", name, description).c_str(),
nullptr, nullptr, nullptr) not_eq SQLITE_OK) {
throw std::runtime_error("Failed to insert new recipe into database.");
}
return get_recipe_id(name);
}
void db::del_recipe(const int id) {
if(not sqlite_db)
throw std::runtime_error(std::format("{}: Database not open! Please contact a developer.", __PRETTY_FUNCTION__));
if(sqlite3_exec(sqlite_db, std::format("DELETE FROM recipes WHERE id={}", id).c_str(),
nullptr, nullptr, nullptr) not_eq SQLITE_OK) {
throw std::runtime_error(std::format("Failed to delete recipe with ID {} from database.", id));
}
}
void db::del_recipes(const std::vector<int> &ids) {
std::string stmt = "DELETE FROM recipes WHERE id IN (";
if(not sqlite_db)
throw std::runtime_error(std::format("{}: Database not open! Please contact a developer.", __PRETTY_FUNCTION__));
bool first = true;
for(auto id : ids) {
if(first)
first = false;
else
stmt += ",";
stmt += std::to_string(id);
}
stmt += ");";
if(sqlite3_exec(sqlite_db, stmt.c_str(), nullptr, nullptr, nullptr) not_eq SQLITE_OK)
throw std::runtime_error("Failed to delete recipes from database.");
}
bool db::recipe_exists(const int id) {
bool exists = false;
if(not sqlite_db)
throw std::runtime_error(std::format("{}: Database not open! Please contact a developer.", __PRETTY_FUNCTION__));
if(sqlite3_exec(sqlite_db, std::format("SELECT id FROM recipes WHERE id={}", id).c_str(),
[](void *found,int,char**,char**) {
*static_cast<bool*>(found) = true;
return 0;
}, &exists, nullptr) not_eq SQLITE_OK) {
throw std::runtime_error("Failed to select from database.");
}
return exists;
}
struct recipe db::get_recipe(const int id) {
struct recipe recipe;
if(not sqlite_db)
throw std::runtime_error(std::format("{}: Database not open! Please contact a developer.", __PRETTY_FUNCTION__));
if(sqlite3_exec(sqlite_db, std::format("SELECT * FROM recipes WHERE id={};", id).c_str(),
[](void *recipe, int, char **col_data, char**) {
*static_cast<struct recipe*>(recipe) = { std::atoi(col_data[0]), col_data[1], col_data[2] };
return 0;
}, &recipe, nullptr) not_eq SQLITE_OK) {
throw std::runtime_error("Failed to select from database.");
}
return recipe;
}
void db::update_recipe_name(const int id, const std::string &new_name) {
if(not sqlite_db)
throw std::runtime_error(std::format("{}: Database not open! Please contact a developer.", __PRETTY_FUNCTION__));
if(sqlite3_exec(sqlite_db, std::format("UPDATE OR IGNORE recipes SET name='{}' WHERE id={};", new_name, id).c_str(),
nullptr, nullptr, nullptr) not_eq SQLITE_OK) {
throw std::runtime_error(std::format("Failed to modify name of recipe with ID {}.", id));
}
}
void db::update_recipe_desc(const int id, const std::string &new_desc) {
if(not sqlite_db)
throw std::runtime_error(std::format("{}: Database not open! Please contact a developer.", __PRETTY_FUNCTION__));
if(sqlite3_exec(sqlite_db, std::format("UPDATE OR IGNORE recipes SET description='{}' WHERE id={};", new_desc, id).c_str(),
nullptr, nullptr, nullptr) not_eq SQLITE_OK) {
throw std::runtime_error(std::format("Failed to modify description of recipe with ID {}.", id));
}
}
std::vector<struct recipe> db::get_recipes(const std::vector<std::string> &ingredients,
const std::vector<std::string> &tags)
{
std::vector<struct recipe> recipes;
std::string stmt = "SELECT id,name,description FROM recipes";
std::string filters;
if(not sqlite_db)
throw std::runtime_error(std::format("{}: Database not open! Please contact a developer.", __PRETTY_FUNCTION__));
if(not ingredients.empty() or not tags.empty())
filters += " WHERE";
if(not ingredients.empty()) {
bool first = true;
for(auto &i : ingredients) {
int id;
if(first)
first = false;
else
filters += " AND";
filters += " id IN (SELECT recipe_id FROM recipe_ingredient WHERE ingredient_id=";
if((id = get_ingredient_id(i)) <= 0)
throw std::runtime_error(std::format("Failed to find ingredient '{}'", i));
filters += std::to_string(id);
filters += ")";
}
}
if(not tags.empty()) {
if(not filters.empty())
filters += " AND";
bool first = true;
for(auto &i : tags) {
int id;
if(first)
first = false;
else
filters += " AND";
filters += " id IN (SELECT recipe_id FROM recipe_tag WHERE tag_id=";
if((id = get_tag_id(i)) <= 0)
throw std::runtime_error("Failed to find tag '{}'");
filters += std::to_string(id);
filters += ")";
}
}
stmt += filters + ";";
if(sqlite3_exec(sqlite_db, stmt.c_str(),
[](void *recipe_list, int, char **col_data, char**) {
static_cast<std::vector<struct recipe>*>(recipe_list)->push_back({
std::atoi(col_data[0]),
col_data[1],
col_data[2] });
return 0;
}, &recipes, nullptr) not_eq SQLITE_OK) {
throw std::runtime_error("Failed to select recipes.");
}
return recipes;
}
int db::add_ingredient(const std::string &name) {
if(not sqlite_db)
throw std::runtime_error(std::format("{}: Database not open! Please contact a developer.", __PRETTY_FUNCTION__));
if(sqlite3_exec(sqlite_db, std::format("INSERT OR IGNORE INTO ingredients(name) VALUES(lower('{}'));", name).c_str(),
nullptr, nullptr, nullptr) not_eq SQLITE_OK) {
throw std::runtime_error(std::format("Failed to instert ingredient '{}'.", name));
}
return get_ingredient_id(name);
}
std::vector<std::string> db::get_recipe_ingredients(const int id) {
std::vector<std::string> ingredients;
if(not sqlite_db)
throw std::runtime_error(std::format("{}: Database not open! Please contact a developer.", __PRETTY_FUNCTION__));
if(sqlite3_exec(sqlite_db, std::format("SELECT name FROM ingredients WHERE id IN (SELECT ingredient_id FROM recipe_ingredient WHERE recipe_id={});", id).c_str(),
[](void *ingredients, int, char **col_data, char**) {
static_cast<std::vector<std::string>*>(ingredients)->push_back(col_data[0]);
return 0;
}, &ingredients, nullptr) not_eq SQLITE_OK) {
throw std::runtime_error(std::format("Failed to select ingredients from recipe with ID {}", id));
}
return ingredients;
}
int db::add_tag(const std::string &name) {
if(not sqlite_db)
throw std::runtime_error(std::format("{}: Database not open! Please contact a developer.", __PRETTY_FUNCTION__));
if(sqlite3_exec(sqlite_db, std::format("INSERT OR IGNORE INTO tags(name) VALUES('{}');", name).c_str(),
nullptr, nullptr, nullptr) not_eq SQLITE_OK) {
throw std::runtime_error(std::format("Failed to insert tag '{}'", name));
}
return get_tag_id(name);
}
std::vector<std::string> db::get_recipe_tags(const int id) {
std::vector<std::string> tags;
if(not sqlite_db)
throw std::runtime_error(std::format("{}: Database not open! Please contact a developer.", __PRETTY_FUNCTION__));
if(sqlite3_exec(sqlite_db, std::format("SELECT name FROM tags WHERE id IN (SELECT tag_id FROM recipe_tag WHERE recipe_id={});", id).c_str(),
[](void *tags, int, char **col_data, char**) {
static_cast<std::vector<std::string>*>(tags)->push_back(col_data[0]);
return 0;
}, &tags, nullptr) not_eq SQLITE_OK) {
throw std::runtime_error(std::format("Failed to select tags for recipe with ID {}", id));
}
return tags;
}
void db::conn_recipe_ingredient(const int recipe_id, const int ingredient_id) {
if(not sqlite_db)
throw std::runtime_error(std::format("{}: Database not open! Please contact a developer.", __PRETTY_FUNCTION__));
if(sqlite3_exec(sqlite_db, std::format("INSERT OR IGNORE INTO recipe_ingredient(recipe_id, ingredient_id) VALUES({},{});", recipe_id, ingredient_id).c_str(),
nullptr, nullptr, nullptr) not_eq SQLITE_OK) {
throw std::runtime_error(std::format("Failed to connect recipe with ID {} to ingredient with ID {}",
recipe_id, ingredient_id));
}
}
void db::disconn_recipe_ingredient(const int recipe_id, const int ingredient_id) {
if(not sqlite_db)
throw std::runtime_error(std::format("{}: Database not open! Please contact a developer.", __PRETTY_FUNCTION__));
if(sqlite3_exec(sqlite_db, std::format("DELETE FROM recipe_ingredient WHERE recipe_id={} AND ingredient_id={};", recipe_id, ingredient_id).c_str(),
nullptr, nullptr, nullptr) not_eq SQLITE_OK) {
throw std::runtime_error(std::format("Failed to disconnect recipe with ID {} from ingredient with ID {}.", recipe_id, ingredient_id));
}
}
void db::conn_recipe_tag(const int recipe_id, const int tag_id) {
if(not sqlite_db)
throw std::runtime_error(std::format("{}: Database not open! Please contact a developer.", __PRETTY_FUNCTION__));
if(sqlite3_exec(sqlite_db, std::format("INSERT OR IGNORE INTO recipe_tag(recipe_id, tag_id) VALUES({},{});", recipe_id, tag_id).c_str(),
nullptr, nullptr, nullptr) not_eq SQLITE_OK) {
throw std::runtime_error(std::format("Failed to connect recipe with ID {} to tag with ID {}",
recipe_id, tag_id));
}
}
void db::disconn_recipe_tag(const int recipe_id, const int tag_id) {
if(not sqlite_db)
throw std::runtime_error(std::format("{}: Database not open! Please contact a developer.", __PRETTY_FUNCTION__));
if(sqlite3_exec(sqlite_db, std::format("DELETE FROM recipe_tag WHERE recipe_id={} AND tag_id={};", recipe_id, tag_id).c_str(),
nullptr, nullptr, nullptr) not_eq SQLITE_OK) {
throw std::runtime_error(std::format("Failed to disconnect recipe with ID {} from tag with ID {}.", recipe_id, tag_id));
}
}

View File

@ -1,64 +0,0 @@
/*
* Copyright (C) 2024 Nicolás Ortega Froysa <nicolas@ortegas.org>
* Nicolás Ortega Froysa <nicolas@ortegas.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
int db_open(void);
void db_close(void);
/**
* @brief Add a new recipe to the database.
*
* @param name Name of the new recipe.
* @param description Short description.
*
* @return ID of newly created recipe, -1 if DB isn't open, -2 on other failure.
*/
int db_add_recipe(const char *name, const char *description);
int db_get_recipe_id(const char *name);
static inline int db_recipe_exists(const char *name) {
return (db_get_recipe_id(name) > 0);
}
/**
* @brief Add a new ingredient to the database.
*
* @param name Name of the new ingredient.
*
* @return ID of newly created ingredient, -1 if DB isn't open, -2 on other failure.
*/
int db_add_ingredient(const char *name);
int db_get_ingredient_id(const char *name);
static inline int db_ingredient_exists(const char *name) {
return (db_get_ingredient_id(name) > 0);
}
/**
* @brief Add a new tag to the database.
*
* @param name Name of the new tag.
*
* @return ID of newly created tag, -1 if DB isn't open, -2 on other failure.
*/
int db_add_tag(const char *name);
int db_get_tag_id(const char *name);
static inline int db_tag_exists(const char *name) {
return (db_get_tag_id(name) > 0);
}
int db_conn_recipe_ingredient(int recipe_id, int ingredient_id);
int db_conn_recipe_tag(int recipe_id, int tag_id);

103
src/db.hpp Normal file
View File

@ -0,0 +1,103 @@
/*
* Copyright (C) 2024 Nicolás Ortega Froysa <nicolas@ortegas.org>
* Nicolás Ortega Froysa <nicolas@ortegas.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <sqlite3.h>
#include <string>
#include <vector>
struct recipe {
int id;
std::string name;
std::string description;
};
class db {
private:
sqlite3 *sqlite_db;
int table_get_id_by_name(const std::string &table, const std::string &name);
public:
db() : sqlite_db(nullptr) {}
~db() {
sqlite3_close(sqlite_db);
}
void open(void);
void close(void);
/**
* @brief Add a new recipe to the database.
*
* @param name Name of the new recipe.
* @param description Short description.
*
* @return ID of newly created recipe.
*/
int add_recipe(const std::string &name, const std::string &description);
void del_recipe(const int id);
void del_recipes(const std::vector<int> &ids);
inline int get_recipe_id(const std::string &name) {
return table_get_id_by_name("recipes", name);
}
inline bool recipe_exists(const std::string &name) {
return (get_recipe_id(name) > 0);
}
bool recipe_exists(const int id);
struct recipe get_recipe(const int id);
void update_recipe_name(const int id, const std::string &new_name);
void update_recipe_desc(const int id, const std::string &new_desc);
std::vector<struct recipe> get_recipes(const std::vector<std::string> &ingredients,
const std::vector<std::string> &tags);
/**
* @brief Add a new ingredient to the database.
*
* @param name Name of the new ingredient.
*
* @return ID of newly created ingredient.
*/
int add_ingredient(const std::string &name);
std::vector<std::string> get_recipe_ingredients(const int id);
inline int get_ingredient_id(const std::string &name) {
return table_get_id_by_name("ingredients", name);
}
inline bool ingredient_exists(const std::string &name) {
return (get_ingredient_id(name) > 0);
}
/**
* @brief Add a new tag to the database.
*
* @param name Name of the new tag.
*
* @return ID of newly created tag, -1 if DB isn't open, -2 on other failure.
*/
int add_tag(const std::string &name);
std::vector<std::string> get_recipe_tags(const int id);
inline int get_tag_id(const std::string &name) {
return table_get_id_by_name("tags", name);
}
inline bool tag_exists(const std::string &name) {
return (get_tag_id(name) > 0);
}
void conn_recipe_ingredient(const int recipe_id, const int ingredient_id);
void disconn_recipe_ingredient(const int recipe_id, const int ingredient_id);
void conn_recipe_tag(const int recipe_id, const int tag_id);
void disconn_recipe_tag(const int recipe_id, const int tag_id);
};

113
src/main.cpp Normal file
View File

@ -0,0 +1,113 @@
/*
* Copyright (C) 2024 Nicolás Ortega Froysa <nicolas@ortegas.org>
* Nicolás Ortega Froysa <nicolas@ortegas.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include <cstdlib>
#include <exception>
#include <iostream>
#include <string>
#include "arg_parse.hpp"
#include "cmd.hpp"
int main(int argc, char *argv[]) {
enum cmd_id id;
int ret = EXIT_SUCCESS;
if(argc < 2) {
std::cerr << "Invalid number of arguments. Use 'help' sub-command." << std::endl;
print_usage();
return EXIT_FAILURE;
}
id = parse_args(argv[1]);
try {
switch(id) {
case CMD_ADD:
if(argc not_eq 2)
throw "Invalid number of arguments. Use 'help' subcommand for more information.";
ret = cmd_add();
break;
case CMD_DEL:
if(argc not_eq 3)
throw "Invalid number of arguments. Use 'help' subcommand for more information.";
ret = cmd_delete(argc - 2, argv + 2);
break;
case CMD_LIST:
if(argc > 6)
throw "Invalid number of arguments. Use 'help' subcommand for more information.";
ret = cmd_list(argc - 1, argv + 1);
break;
case CMD_INFO:
if(argc not_eq 3)
throw "Invalid number of arguments. Use 'help' subcommand for more information.";
ret = cmd_info(std::stoi(argv[2]));
break;
case CMD_EDIT_NAME:
if(argc not_eq 3)
throw "Invalid number of arguments. Use 'help' subcommand for more information.";
ret = cmd_edit_name(std::stoi(argv[2]));
break;
case CMD_EDIT_DESC:
if(argc not_eq 3)
throw "Invalid number of arguments. Use 'help' subcommand for more information.";
ret = cmd_edit_desc(std::stoi(argv[2]));
break;
case CMD_ADD_INGR:
if(argc not_eq 4)
throw "Invalid number of arguments. Use 'help' subcommand for more information.";
ret = cmd_add_ingr(std::stoi(argv[2]), argv[3]);
break;
case CMD_RM_INGR:
if(argc not_eq 4)
throw "Invalid number of arguments. Use 'help' subcommand for more information.";
ret = cmd_rm_ingr(std::stoi(argv[2]), argv[3]);
break;
case CMD_ADD_TAG:
if(argc not_eq 4)
throw "Invalid number of arguments. Use 'help' subcommand for more information.";
ret = cmd_add_tag(std::stoi(argv[2]), argv[3]);
break;
case CMD_RM_TAG:
if(argc not_eq 4)
throw "Invalid number of arguments. Use 'help' subcommand for more information.";
ret = cmd_rm_tag(std::stoi(argv[2]), argv[3]);
break;
case CMD_HELP:
if(argc not_eq 2)
throw "Invalid number of arguments. Use 'help' subcommand for more information.";
print_help();
break;
case CMD_VERSION:
if(argc not_eq 2)
throw "Invalid number of arguments. Use 'help' subcommand for more information.";
print_version();
break;
default:
std::cerr << "No such command '" << argv[1] << "'. Use 'help' sub-command." << std::endl;
print_usage();
ret = EXIT_FAILURE;
break;
}
} catch(const std::exception &e) {
std::cerr << e.what() << std::endl;
ret = EXIT_FAILURE;
}
return ret;
}

View File

@ -15,38 +15,31 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdlib.h>
#include <stdio.h>
#include "util.hpp"
#include <algorithm>
#include <cctype>
#include "arg_parse.h"
#include "cmd.h"
std::vector<std::string> split(std::string str, const std::string &delim) {
std::vector<std::string> result;
std::string substr;
size_t pos = 0;
int main(int argc, char *argv[]) {
enum cmd_id id;
if(argc < 2) {
fprintf(stderr, "Invalid number of arguments. Use 'help' sub-command.\n");
print_usage();
return EXIT_FAILURE;
while((pos = str.find(delim)) not_eq std::string::npos) {
substr = str.substr(0, pos);
result.push_back(substr);
str.erase(0, pos + delim.size());
}
result.push_back(str);
id = parse_args(argv[1]);
switch(id) {
case CMD_ADD:
command_add();
break;
case CMD_HELP:
print_help();
break;
case CMD_VERSION:
print_version();
break;
default:
fprintf(stderr, "No such command '%s'. Use 'help' sub-command.\n", argv[1]);
print_usage();
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
return result;
}
void trim(std::string &str) {
str.erase(str.begin(),
std::find_if(str.begin(), str.end(), [](char c) {
return not std::isspace(c);
}));
str.erase(std::find_if(str.rbegin(), str.rend(), [](char c) {
return not std::isspace(c);
}).base(), str.end());
}

View File

@ -17,4 +17,8 @@
*/
#pragma once
#define ARRAY_LEN(arr) (sizeof(arr) / sizeof(arr[0]))
#include <vector>
#include <string>
std::vector<std::string> split(std::string str, const std::string &delim);
void trim(std::string &str);