Compare commits
40 Commits
ca544e74fa
...
v1.0
Author | SHA1 | Date | |
---|---|---|---|
3bb7f52823 | |||
93da965805 | |||
4d07cdf4d4 | |||
edb5d29040 | |||
61136eced5 | |||
c6c3b45ec9 | |||
6158aaf673 | |||
49bb2f4fc8 | |||
4379b0311e | |||
a3b6471c13 | |||
907f8a7b41 | |||
009e3f09bf | |||
320346911c | |||
0b348e8c10 | |||
f68a8b45a5 | |||
feed35d8c8 | |||
db0b607546 | |||
d8a140e913 | |||
a1e3322576 | |||
802f3bfcc0 | |||
5873ceb627 | |||
5bcc598880 | |||
03b1250006 | |||
c71467804e | |||
c31ccdece2 | |||
d2f30dcca7 | |||
2434a516cd | |||
d30f8df5c1 | |||
ba7f930231 | |||
e92a578d65 | |||
1a590b477e | |||
c0c6959774 | |||
08b4834d9e | |||
e25fb66dbc | |||
5f53180a60 | |||
deb3f2d13f | |||
81eced2636 | |||
b5094d68e9 | |||
e86ca506c1 | |||
836daeb69b |
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@
|
||||
/menu-helper
|
||||
/compile_commands.json
|
||||
.cache/*
|
||||
/*.1.gz
|
||||
|
20
Makefile
20
Makefile
@ -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,9 @@ clean:
|
||||
$(RM) $(OBJS)
|
||||
|
||||
distclean: clean
|
||||
$(RM) menu-helper.1.gz
|
||||
$(RM) menu-helper
|
||||
|
||||
install: menu-helper
|
||||
install: menu-helper menu-helper.1.gz
|
||||
install -m 755 menu-helper $(PREFIX)/bin/
|
||||
install -m 644 menu-helper.1.gz $(PREFIX)/share/man/man1/
|
||||
|
176
README.md
176
README.md
@ -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.
|
||||
|
14
TODO.md
Normal file
14
TODO.md
Normal file
@ -0,0 +1,14 @@
|
||||
# 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
|
||||
|
||||
- [ ] Add import/export functionality.
|
||||
- [ ] Allow for writing description in editor.
|
65
menu-helper.1
Normal file
65
menu-helper.1
Normal 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.
|
@ -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;
|
||||
}
|
@ -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
85
src/arg_parse.hpp
Normal 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
100
src/cmd.c
@ -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
339
src/cmd.cpp
Normal 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;
|
||||
}
|
@ -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
221
src/db.c
@ -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
368
src/db.cpp
Normal 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));
|
||||
}
|
||||
}
|
64
src/db.h
64
src/db.h
@ -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
103
src/db.hpp
Normal 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
113
src/main.cpp
Normal 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;
|
||||
}
|
@ -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());
|
||||
}
|
@ -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);
|
Reference in New Issue
Block a user