23 Commits

Author SHA1 Message Date
f16986ce26 refacto: explicit dotenv import in sesam-vitale/build.rs 2024-08-30 18:28:29 +02:00
f56439c9c5 feat: initialize a utils lib with config functions handling config files in local and standard OS directories 2024-08-30 18:28:29 +02:00
90ff593438 Merge pull request 'Implémentation "HATEOAS" de l'interface pour HTMX et update des URLs qui fonctionne !' (#57) from feat/54_update_url_on_navbar_navigation into main
Reviewed-on: P4Pillon/Krys4lide#57
Reviewed-by: kosssi <simon@p4pillon.org>

Implémente une approche plus respectueuse de "HATEOAS" comme nécessité par HTMX.

Dans cette approche, les routes des pages, comme /index ou /cps, ont, par défaut, un rendu "complet", comme cela serait si l'on n'utilisait pas HTMX.
Certains helpers sont utilisés pour éviter la duplication de code (entre particulier une "macro" Jinja pour la navbar).

Un "Extractor" issu de axum-htmx permet d'identifier quand une requête sur ces pages est issue d'un appel htmx, et permet de générer un rendu "simplifié", ne contenant (quasiment) que les éléments HTML ayant leur contenu qui change.
Par exemple, la page /cps, dans le cadre d'une requête htmx (hx-request=true), ne retourne que : une nouvelle balise <title>, la navbar (pour changer le design de l'élément "courant") et le contenu de la page. Tout le contenu de <head> ou les div qui encapsulent l'ensemble de la page ne sont pas re-envoyé.

Cela permet de mettre à jour les URLs dans la barre du navigateur, sans provoquer les problèmes de rechargement de page identifié dans le ticket #54.

Au passage, cette PR organise les templates d'une manière qui me parait un peu plus "claire" qu'avant :

- Les "pages", correspondant à des routes réelles, sont définies dans app/src/pages
- Les "composants", comme la navbar, qui ont vocation à être "inséré" (plutôt par des includes ou des appels de fonctions), sont définis dans app/src/components

L'avantage de leur présence dans le dossier src est qu'on peut sans problème mettre à côté les fichiers .rs qui leur correspondent.

La PR #53 et le ticket #54 avaient éclairé un problème lorsqu'on souhaitait pouvoir refléter la navigation dans l'URL du navigateur. Cette PR corrige ce problème en adoptant une approche plus respectueuse de la philosophie de HTMX.

- La nouvelle librairie axum-htmx qui offre des helpers pour faire du HTMX dans Axum
- Un repo' d'exemple intéressant, du même auteur, qui propose certaines approches pour gérer les "partials" (le rendu différentiel selon qu'on a une requête HTMX ou pas)

Closes #46
Closes #54
2024-08-30 18:25:53 +02:00
216eb73757 fixup! refacto: move home code into a dedicated file and rename index to home everywhere 2024-08-27 11:28:36 +02:00
d4e565601a fix: make darkmode work by removing hardcoded tailwindcss from flowbite 2024-08-27 11:19:59 +02:00
c39ae44d74 docs: add some comments on useful locations 2024-08-27 11:19:59 +02:00
b7fcfe3792 refacto: move home code into a dedicated file and rename index to home everywhere 2024-08-27 11:19:59 +02:00
ab908f2664 fix: Small translations and misspelled IDs 2024-08-27 11:19:59 +02:00
7d4dc81df2 feat: implement htmx with partials on index and cps pages 2024-08-27 11:19:59 +02:00
2236a7219b refacto: remove old_* directories 2024-08-27 11:19:59 +02:00
3e9e8ecacc chore: update style.css 2024-08-27 11:19:59 +02:00
8ce18e53d5 feat: Rewrite routes, pages and components to be more HATEOAS 2024-08-27 11:19:59 +02:00
7487b34a17 refacto: rename html and rs templates dirs to old_* 2024-08-27 11:19:59 +02:00
217667253a feat: Add hx-push-url attribute to nav menu item 2024-08-27 11:19:59 +02:00
6dbf5b5438 feat: Add HX-Request header extraction to CPS endpoint 2024-08-27 11:19:59 +02:00
0e2e863bc0 Merge pull request 'fix: #50 - Correction des noms de module utilisés dans le template gitea de rapport de bug' (#59) from fix/50_use_correct_crates_labels_on_gitea into main
Reviewed-on: P4Pillon/Krys4lide#59
Reviewed-by: kosssi <simon@p4pillon.org>

Les noms utilisés dans le template par défaut lors d'une création d'un ticket de Bug étaient les "anciens" noms (Cargo, Tauri ...)

Cette PR :
- Ajoute quelques options (docs, scripts ...)
- Remplace les anciens noms par des labels plus compréhensifs et les noms des "crates" ou dossiers correspondant
2024-08-27 10:43:16 +02:00
307bdf8fa6 fix: update module options in BUG_REPORT.yaml template 2024-08-26 22:36:53 +02:00
32009e2f00 Merge pull request 'Implémentation d'une première approche de gestion des erreurs' (#52) from feat/handle_errors_rustly into main
# Détails

Implémente (en partie) les "bonnes pratiques" de gestion des erreurs Rust décrites dans le ticket #34

Cette PR se concentre beaucoup sur la gestion des erreurs "internes" et peu sur la manière dont on les expose à l'extérieur (API) ou les trace (dans des logs ou sur une app de suivi de logs). Une deuxième phase de travail sera nécessaire pour cela.

De même, les choix faits à de nombreux endroits sont très perfectibles :

- les noms des erreurs et enums ne semblent pas toujours au top
- certains "fallback" pourraient sans doute être améliorés
- certaines erreurs pourraient peut-être remonter + de contexte
- l'usage des librairies d'aide (anyhow et thiserror) ne sont pas uniformes

Cela dit, il est difficile de vraiment affiner ces éléments sans être dans des situations concrètes et réalistes. Je pense donc que c'est à force d'itération, en situation concrète et au fur et à mesure des relectures de code en DA que nous améliorerons ces points.

# Pourquoi ?

La gestion des erreurs en Rust est extrêmement puissante, permettant de construire des infrastructures où les "bugs" inattendus sont quasiment inexistants. Pour cela, un certain nombre de bonnes pratiques sont à suivre dans la gestion des retours d'erreurs des fonctions et des programmes à plus haut niveau.

# Documentation

CF #34

Reviewed-on: P4Pillon/Krys4lide#52
Reviewed-by: theo <theo.lettermann@gmail.com>
Reviewed-by: kosssi <simon@p4pillon.org>
2024-08-22 20:56:12 +02:00
1561fd2a44 feat: add documentation about errors handling in docs/errors.md 2024-08-20 22:33:57 +02:00
4d9f6e2638 chore: update Cargo.lock according to previous branch commits 2024-08-15 19:30:28 +02:00
760a9cd92c feat: [sesam-vitale] Use thiserror, anyhow and expect to properly handle errors instead of unwrap 2024-08-15 19:28:13 +02:00
3f476c3114 feat: [desktop] Use thiserror and expect to properly handle errors instead of unwrap 2024-08-14 10:58:09 +02:00
d44c561427 feat: [app] Use thiserror to properly handle errors instead of unwrap 2024-08-14 10:40:41 +02:00
70 changed files with 1056 additions and 1894 deletions

View File

@ -24,6 +24,10 @@ body:
label: Ce problème est il relatif à un ou des modules en particulier ?
multiple: true
options:
- Clego
- Tauri
- Axum
- Interface utilisateur⋅ice (crates/app)
- Encapsulation Tauri (crates/desktop)
- Moteur SESAM-Vitale (crates/sesam-vitale)
- Librairie utilitaire (crates/utils)
- Documentation (docs)
- Scripts (scripts)
- Autre

2
.gitignore vendored
View File

@ -21,3 +21,5 @@ target/
*.sln
*.sw?
# Ignore .env files
.env

53
Cargo.lock generated
View File

@ -87,11 +87,13 @@ dependencies = [
"askama",
"askama_axum",
"axum",
"axum-htmx",
"cargo-watch",
"listenfd",
"notify 6.1.1",
"serde",
"systemfd",
"thiserror",
"tokio",
"tower-http",
"tower-livereload",
@ -397,6 +399,20 @@ dependencies = [
"tracing",
]
[[package]]
name = "axum-htmx"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36cdb6062317f732ed3acf4e9c28c3824092e226726616f46ebdd8cd32c82a41"
dependencies = [
"async-trait",
"axum-core",
"futures",
"http",
"tokio",
"tower",
]
[[package]]
name = "backtrace"
version = "0.3.73"
@ -1066,6 +1082,7 @@ dependencies = [
"http",
"tauri",
"tauri-build",
"thiserror",
"tokio",
"tower",
]
@ -1080,6 +1097,15 @@ dependencies = [
"crypto-common",
]
[[package]]
name = "directories"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35"
dependencies = [
"dirs-sys 0.4.1",
]
[[package]]
name = "dirs"
version = "4.0.0"
@ -1468,6 +1494,20 @@ dependencies = [
"new_debug_unreachable",
]
[[package]]
name = "futures"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.30"
@ -1475,6 +1515,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
@ -3915,8 +3956,11 @@ dependencies = [
name = "sesam-vitale"
version = "0.1.0"
dependencies = [
"anyhow",
"dotenv",
"libc",
"thiserror",
"utils",
]
[[package]]
@ -5034,6 +5078,15 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utils"
version = "0.1.0"
dependencies = [
"anyhow",
"directories",
"dotenv",
]
[[package]]
name = "uuid"
version = "0.8.2"

View File

@ -3,5 +3,6 @@ resolver = "2"
members = [
"crates/app",
"crates/sesam-vitale",
"crates/desktop"
"crates/desktop",
"crates/utils",
]

View File

@ -7,6 +7,20 @@ Logiciel de Pharmacie libre et open-source.
- `app`: Interface du logiciel, servie par un serveur web propulsé par Axum. Utilisable en mode endpoint ou encapsulé dans le client `desktop`
- `desktop`: Client desktop propulsé par Tauri, encapsulant le serveur web `app`
- `sesam-vitale`: Bibliothèque de gestion des services SESAM-Vitale (Lecture des cartes CPS et Vitale, téléservices ...)
- `utils`: Bibliothèque de fonctions utilitaires
## Installation
### Fichiers de configuration
Certaines librairies nécessitent de définir certaines paramètres de configuration pour fonctionner correctement, en particulier le moteur SESAM-Vitale.
Ces paramètres sont définis dans un fichier de configuration `.env` situé dans un des dossiers suivant (par ordre de priorité) :
- dans le dossier courant (`./.env`)
- dans le dossier du manifeste (par exemple `crates/sesam-vitale/.env`)
- dans le dossier de configuration standard de l'OS (par exemple, sur linux, `~/.config/krys4lide/.env` - [plus d'info](https://github.com/dirs-dev/directories-rs?tab=readme-ov-file#projectdirs))
Des exemples de fichiers de configuration sont disponibles à la racine du projet : `.env.linux.example` et `.env.win.example`.
## Development

View File

@ -7,9 +7,11 @@ edition = "2021"
askama = "0.12.1"
askama_axum = "0.4.0"
axum = "0.7.5"
axum-htmx = { version = "0.6", features = ["auto-vary"] }
listenfd = "1.0.1"
notify = "6.1.1"
serde = { version = "1.0.204", features = ["derive"] }
thiserror = "1.0.63"
tokio = { version = "1.39.1", features = ["macros", "rt-multi-thread"] }
tower-http = { version = "0.5.2", features = ["fs"] }
tower-livereload = "0.9.3"

6
crates/app/askama.toml Normal file
View File

@ -0,0 +1,6 @@
[general]
# Directories to search for templates, relative to the crate root.
dirs = [
"src/pages",
"src/components",
]

File diff suppressed because one or more lines are too long

View File

@ -566,28 +566,8 @@ video {
border-width: 0;
}
.absolute {
position: absolute;
}
.relative {
position: relative;
}
.-inset-0\.5 {
inset: -0.125rem;
}
.-inset-1\.5 {
inset: -0.375rem;
}
.right-0 {
right: 0px;
}
.z-10 {
z-index: 10;
.z-50 {
z-index: 50;
}
.mx-auto {
@ -595,36 +575,9 @@ video {
margin-right: auto;
}
.-mr-2 {
margin-right: -0.5rem;
}
.ml-3 {
margin-left: 0.75rem;
}
.ml-auto {
margin-left: auto;
}
.mt-2 {
margin-top: 0.5rem;
}
.mt-3 {
margin-top: 0.75rem;
}
.mb-4 {
margin-bottom: 1rem;
}
.mt-4 {
.my-4 {
margin-top: 1rem;
}
.mt-6 {
margin-top: 1.5rem;
margin-bottom: 1rem;
}
.mb-2 {
@ -635,10 +588,22 @@ video {
margin-bottom: 0.625rem;
}
.mb-4 {
margin-bottom: 1rem;
}
.me-3 {
margin-inline-end: 0.75rem;
}
.mt-3 {
margin-top: 0.75rem;
}
.mt-4 {
margin-top: 1rem;
}
.block {
display: block;
}
@ -663,52 +628,44 @@ video {
height: 2.5rem;
}
.h-16 {
height: 4rem;
}
.h-6 {
height: 1.5rem;
}
.h-8 {
height: 2rem;
}
.h-full {
height: 100%;
}
.h-32 {
height: 8rem;
}
.h-48 {
height: 12rem;
}
.h-96 {
height: 24rem;
.h-2 {
height: 0.5rem;
}
.h-2\.5 {
height: 0.625rem;
}
.h-9 {
height: 2.25rem;
.h-32 {
height: 8rem;
}
.h-4 {
height: 1rem;
}
.h-48 {
height: 12rem;
}
.h-5 {
height: 1.25rem;
}
.h-7 {
height: 1.75rem;
}
.h-2 {
height: 0.5rem;
.h-8 {
height: 2rem;
}
.h-4 {
height: 1rem;
.h-96 {
height: 24rem;
}
.h-full {
height: 100%;
}
.min-h-full {
@ -719,58 +676,38 @@ video {
width: 2.5rem;
}
.w-32 {
width: 8rem;
}
.w-48 {
width: 12rem;
}
.w-6 {
width: 1.5rem;
.w-5 {
width: 1.25rem;
}
.w-8 {
width: 2rem;
}
.w-auto {
width: auto;
}
.w-32 {
width: 8rem;
}
.w-20 {
width: 5rem;
}
.w-24 {
width: 6rem;
}
.w-28 {
width: 7rem;
.w-full {
width: 100%;
}
.max-w-7xl {
max-width: 80rem;
}
.max-w-xs {
max-width: 20rem;
.max-w-screen-xl {
max-width: 1280px;
}
.max-w-sm {
max-width: 24rem;
}
.flex-shrink-0 {
flex-shrink: 0;
}
.origin-top-right {
transform-origin: top right;
}
@keyframes pulse {
50% {
opacity: .5;
@ -781,12 +718,20 @@ video {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
.list-none {
list-style-type: none;
}
.grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
.flex-col {
flex-direction: column;
}
.flex-wrap {
flex-wrap: wrap;
}
.items-center {
@ -805,48 +750,66 @@ video {
gap: 1rem;
}
.space-y-1 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(0.25rem * var(--tw-space-y-reverse));
.space-x-3 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(0.75rem * var(--tw-space-x-reverse));
margin-left: calc(0.75rem * calc(1 - var(--tw-space-x-reverse)));
}
.rounded-full {
border-radius: 9999px;
.divide-y > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0;
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
}
.rounded-md {
border-radius: 0.375rem;
.divide-gray-100 > :not([hidden]) ~ :not([hidden]) {
--tw-divide-opacity: 1;
border-color: rgb(243 244 246 / var(--tw-divide-opacity));
}
.rounded-lg {
border-radius: 0.5rem;
.self-center {
align-self: center;
}
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.whitespace-nowrap {
white-space: nowrap;
}
.rounded {
border-radius: 0.25rem;
}
.border-2 {
border-width: 2px;
.rounded-full {
border-radius: 9999px;
}
.rounded-lg {
border-radius: 0.5rem;
}
.border {
border-width: 1px;
}
.border-b {
border-bottom-width: 1px;
}
.border-t {
border-top-width: 1px;
.border-2 {
border-width: 2px;
}
.border-dashed {
border-style: dashed;
}
.border-gray-100 {
--tw-border-opacity: 1;
border-color: rgb(243 244 246 / var(--tw-border-opacity));
}
.border-gray-200 {
--tw-border-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-border-opacity));
@ -857,9 +820,9 @@ video {
border-color: rgb(209 213 219 / var(--tw-border-opacity));
}
.bg-white {
.bg-blue-700 {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
background-color: rgb(29 78 216 / var(--tw-bg-opacity));
}
.bg-gray-200 {
@ -872,8 +835,19 @@ video {
background-color: rgb(209 213 219 / var(--tw-bg-opacity));
}
.p-1 {
padding: 0.25rem;
.bg-gray-50 {
--tw-bg-opacity: 1;
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
}
.bg-gray-800 {
--tw-bg-opacity: 1;
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
}
.bg-white {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
.p-2 {
@ -884,36 +858,39 @@ video {
padding: 1rem;
}
.px-3 {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.py-1 {
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.py-10 {
padding-top: 2.5rem;
padding-bottom: 2.5rem;
}
.py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.py-3 {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
}
.py-8 {
padding-top: 2rem;
padding-bottom: 2rem;
}
.pb-3 {
padding-bottom: 0.75rem;
}
.pt-2 {
padding-top: 0.5rem;
}
.pt-4 {
padding-top: 1rem;
.text-2xl {
font-size: 1.5rem;
line-height: 2rem;
}
.text-3xl {
@ -939,6 +916,10 @@ video {
font-weight: 500;
}
.font-semibold {
font-weight: 600;
}
.leading-tight {
line-height: 1.25;
}
@ -947,9 +928,9 @@ video {
letter-spacing: -0.025em;
}
.text-gray-400 {
.text-gray-200 {
--tw-text-opacity: 1;
color: rgb(156 163 175 / var(--tw-text-opacity));
color: rgb(229 231 235 / var(--tw-text-opacity));
}
.text-gray-500 {
@ -957,9 +938,9 @@ video {
color: rgb(107 114 128 / var(--tw-text-opacity));
}
.text-gray-800 {
.text-gray-700 {
--tw-text-opacity: 1;
color: rgb(31 41 55 / var(--tw-text-opacity));
color: rgb(55 65 81 / var(--tw-text-opacity));
}
.text-gray-900 {
@ -967,19 +948,9 @@ video {
color: rgb(17 24 39 / var(--tw-text-opacity));
}
.text-gray-200 {
.text-white {
--tw-text-opacity: 1;
color: rgb(229 231 235 / var(--tw-text-opacity));
}
.opacity-0 {
opacity: 0;
}
.shadow-lg {
--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.shadow {
@ -988,31 +959,11 @@ video {
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.ring-1 {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.ring-black {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(0 0 0 / var(--tw-ring-opacity));
}
.ring-opacity-5 {
--tw-ring-opacity: 0.05;
}
.hover\:bg-gray-100:hover {
--tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
}
.hover\:text-gray-500:hover {
--tw-text-opacity: 1;
color: rgb(107 114 128 / var(--tw-text-opacity));
}
.focus\:outline-none:focus {
outline: 2px solid transparent;
outline-offset: 2px;
@ -1024,47 +975,27 @@ video {
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.focus\:ring-indigo-500:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(99 102 241 / var(--tw-ring-opacity));
.focus\:ring-4:focus {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.focus\:ring-offset-2:focus {
--tw-ring-offset-width: 2px;
.focus\:ring-gray-200:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(229 231 235 / var(--tw-ring-opacity));
}
.focus\:ring-gray-300:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(209 213 219 / var(--tw-ring-opacity));
}
@media (min-width: 640px) {
.sm\:-my-px {
margin-top: -1px;
margin-bottom: -1px;
}
.sm\:ml-6 {
margin-left: 1.5rem;
}
.sm\:flex {
display: flex;
}
.sm\:hidden {
display: none;
}
.sm\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.sm\:items-center {
align-items: center;
}
.sm\:space-x-8 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(2rem * var(--tw-space-x-reverse));
margin-left: calc(2rem * calc(1 - var(--tw-space-x-reverse)));
}
.sm\:px-6 {
padding-left: 1.5rem;
padding-right: 1.5rem;
@ -1072,28 +1003,91 @@ video {
}
@media (min-width: 768px) {
.md\:order-1 {
order: 1;
}
.md\:order-2 {
order: 2;
}
.md\:me-0 {
margin-inline-end: 0px;
}
.md\:mt-0 {
margin-top: 0px;
}
.md\:flex {
display: flex;
}
.md\:hidden {
display: none;
}
.md\:h-64 {
height: 16rem;
}
.md\:h-72 {
height: 18rem;
.md\:w-auto {
width: auto;
}
.md\:flex-row {
flex-direction: row;
}
.md\:space-x-0 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(0px * var(--tw-space-x-reverse));
margin-left: calc(0px * calc(1 - var(--tw-space-x-reverse)));
}
.md\:space-x-8 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(2rem * var(--tw-space-x-reverse));
margin-left: calc(2rem * calc(1 - var(--tw-space-x-reverse)));
}
.md\:border-0 {
border-width: 0px;
}
.md\:bg-transparent {
background-color: transparent;
}
.md\:bg-white {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
.md\:p-0 {
padding: 0px;
}
.md\:p-6 {
padding: 1.5rem;
}
.md\:text-blue-700 {
--tw-text-opacity: 1;
color: rgb(29 78 216 / var(--tw-text-opacity));
}
.md\:hover\:bg-transparent:hover {
background-color: transparent;
}
.md\:hover\:text-blue-700:hover {
--tw-text-opacity: 1;
color: rgb(29 78 216 / var(--tw-text-opacity));
}
}
@media (min-width: 1024px) {
.lg\:block {
display: block;
}
.lg\:hidden {
display: none;
}
.lg\:grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
@ -1104,7 +1098,16 @@ video {
}
}
.rtl\:space-x-reverse:where([dir="rtl"], [dir="rtl"] *) > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 1;
}
@media (prefers-color-scheme: dark) {
.dark\:divide-gray-600 > :not([hidden]) ~ :not([hidden]) {
--tw-divide-opacity: 1;
border-color: rgb(75 85 99 / var(--tw-divide-opacity));
}
.dark\:border-gray-600 {
--tw-border-opacity: 1;
border-color: rgb(75 85 99 / var(--tw-border-opacity));
@ -1120,6 +1123,26 @@ video {
background-color: rgb(55 65 81 / var(--tw-bg-opacity));
}
.dark\:bg-gray-800 {
--tw-bg-opacity: 1;
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
}
.dark\:bg-gray-900 {
--tw-bg-opacity: 1;
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
}
.dark\:text-gray-200 {
--tw-text-opacity: 1;
color: rgb(229 231 235 / var(--tw-text-opacity));
}
.dark\:text-gray-400 {
--tw-text-opacity: 1;
color: rgb(156 163 175 / var(--tw-text-opacity));
}
.dark\:text-gray-600 {
--tw-text-opacity: 1;
color: rgb(75 85 99 / var(--tw-text-opacity));
@ -1129,4 +1152,52 @@ video {
--tw-text-opacity: 1;
color: rgb(55 65 81 / var(--tw-text-opacity));
}
.dark\:text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.dark\:hover\:bg-gray-600:hover {
--tw-bg-opacity: 1;
background-color: rgb(75 85 99 / var(--tw-bg-opacity));
}
.dark\:hover\:bg-gray-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(55 65 81 / var(--tw-bg-opacity));
}
.dark\:hover\:text-white:hover {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.dark\:focus\:ring-gray-600:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(75 85 99 / var(--tw-ring-opacity));
}
}
@media (min-width: 768px) {
@media (prefers-color-scheme: dark) {
.md\:dark\:bg-gray-900 {
--tw-bg-opacity: 1;
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
}
.md\:dark\:text-blue-500 {
--tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity));
}
.md\:dark\:hover\:bg-transparent:hover {
background-color: transparent;
}
.md\:dark\:hover\:text-blue-500:hover {
--tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity));
}
}
}

View File

@ -1,3 +1,7 @@
{% if hx_request %}
<title>{% block title %}{{ title }}{% endblock %}</title>
{% block body %}{% endblock %}
{% else %}
<!doctype html>
<html lang="fr" class="h-full">
<head>
@ -5,19 +9,15 @@
<script src="/assets/js/htmx@2.0.1.min.js"></script>
<script src="/assets/js/alpinejs@3.14.1.min.js" defer></script>
<link href="/assets/css/style.css" rel="stylesheet">
<link href="/assets/css/flowbite@2.5.1.min.css" rel="stylesheet" />
<script src="/assets/js/flowbite@2.5.1.min.js"></script>
<link href="/assets/css/style.css" rel="stylesheet">
{% block head %}{% endblock %}
</head>
<body class="h-full">
<div class="min-h-full">
{% block nav %}
{% include "layout/nav.html" %}
{% endblock %}
{% block body %}{% endblock %}
</div>
</body>
</html>
{% endif %}

View File

@ -0,0 +1,18 @@
{% set selected = item.id == current %}
<li>
<a
href="{{ item.href }}"
{% if selected -%}
class="block py-2 px-3 text-white bg-blue-700 rounded md:bg-transparent md:text-blue-700 md:p-0 md:dark:text-blue-500"
aria-current="page"
{% else -%}
class="block py-2 px-3 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
{% endif -%}
hx-get="{{ item.href }}"
hx-push-url="true"
hx-swap="outerHTML"
hx-select-oob="#menu-items,#page-header,#page-main"
>
{{ item.label }}
</a>
</li>

View File

@ -0,0 +1,50 @@
{% macro navbar(current) %}
{% let items=crate::menu::get_menu_items() %}
<nav class="bg-white border-gray-200 dark:bg-gray-900">
<div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
<a href="/" class="flex items-center space-x-3 rtl:space-x-reverse">
<img src="https://flowbite.com/docs/images/logo.svg" class="h-8" alt="Flowbite Logo" />
<span class="self-center text-2xl font-semibold whitespace-nowrap dark:text-white">Krys4lide</span>
</a>
<div class="flex items-center md:order-2 space-x-3 md:space-x-0 rtl:space-x-reverse">
<button type="button" class="flex text-sm bg-gray-800 rounded-full md:me-0 focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600" id="user-menu-button" aria-expanded="false" data-dropdown-toggle="user-dropdown" data-dropdown-placement="bottom">
<span class="sr-only">Ouvrir le menu de profil</span>
<img class="w-8 h-8 rounded-full" src="https://flowbite.com/docs/images/people/profile-picture-3.jpg" alt="user photo">
</button>
<!-- Dropdown menu -->
<div class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded-lg shadow dark:bg-gray-700 dark:divide-gray-600" id="user-dropdown">
<div class="px-4 py-3">
<span class="block text-sm text-gray-900 dark:text-white">Bonnie Green</span>
<span class="block text-sm text-gray-500 truncate dark:text-gray-400">name@flowbite.com</span>
</div>
<ul class="py-2" aria-labelledby="user-menu-button">
<li>
<a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Profile</a>
</li>
<li>
<a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Settings</a>
</li>
<li>
<a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Sign out</a>
</li>
</ul>
</div>
<button data-collapse-toggle="navbar-user" type="button" class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600" aria-controls="navbar-user" aria-expanded="false">
<span class="sr-only">Ouvrir le menu de navigation</span>
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15"/>
</svg>
</button>
</div>
<div class="items-center justify-between hidden w-full md:flex md:w-auto md:order-1" id="navbar-user">
<ul id="menu-items" class="flex flex-col font-medium p-4 md:p-0 mt-4 border border-gray-100 rounded-lg bg-gray-50 md:space-x-8 rtl:space-x-reverse md:flex-row md:mt-0 md:border-0 md:bg-white dark:bg-gray-800 md:dark:bg-gray-900 dark:border-gray-700">
{% for item in items %}
{% include "navbar/menu-item.html" %}
{% endfor %}
</ul>
</div>
</div>
</nav>
{% endmacro %}

View File

@ -1,29 +1,21 @@
mod pages;
mod templates;
use std::path::Path;
use askama_axum::Template;
use axum::http::{StatusCode, Uri};
use axum_htmx::AutoVaryLayer;
use tower_http::services::ServeDir;
mod menu;
mod pages;
async fn fallback(uri: Uri) -> (StatusCode, String) {
(StatusCode::NOT_FOUND, format!("No route for {uri}"))
}
#[derive(Template)]
#[template(path = "index.html")]
pub struct GetIndexTemplate;
async fn root() -> GetIndexTemplate {
GetIndexTemplate {}
}
pub fn get_router(assets_path: &Path) -> axum::Router {
axum::Router::new()
.nest_service("/assets", ServeDir::new(assets_path))
.route("/", axum::routing::get(root))
.nest("/pages", pages::get_routes())
.merge(templates::get_routes())
.merge(pages::get_routes())
.fallback(fallback)
// The AutoVaryLayer is used to avoid cache issues with htmx (cf: https://github.com/robertwayne/axum-htmx?tab=readme-ov-file#auto-caching-management)
.layer(AutoVaryLayer)
}

View File

@ -1,14 +1,27 @@
use ::app::get_router;
use std::path::{Path, PathBuf};
use std::{env, io};
use axum::body::Body;
use axum::http::Request;
use listenfd::ListenFd;
use notify::Watcher;
use std::env;
use std::path::Path;
use thiserror::Error;
use tokio::net::TcpListener;
use tower_livereload::predicate::Predicate;
use tower_livereload::LiveReloadLayer;
use ::app::get_router;
#[derive(Error, Debug)]
pub enum AppError {
#[error("Unable to bind to TCP listener")]
TCPListener(#[from] std::io::Error),
#[error("Error with the notify watcher")]
NotifyWatcher(#[from] notify::Error),
#[error("Missing environment variable {var}")]
MissingEnvVar { var: &'static str },
}
/// Nous filtrons les requêtes de `htmx` pour ne pas inclure le script _JS_ qui gère le rechargement
/// Voir https://github.com/leotaku/tower-livereload/pull/3
#[derive(Copy, Clone)]
@ -20,44 +33,52 @@ impl<T> Predicate<Request<T>> for NotHtmxPredicate {
}
const DEFAULT_LISTENER: &str = "localhost:3000";
async fn get_tcp_listener() -> TcpListener {
async fn get_tcp_listener() -> Result<TcpListener, io::Error> {
let mut listenfd = ListenFd::from_env();
match listenfd.take_tcp_listener(0).unwrap() {
match listenfd.take_tcp_listener(0)? {
// if we are given a tcp listener on listen fd 0, we use that one
Some(listener) => {
listener.set_nonblocking(true).unwrap();
TcpListener::from_std(listener).unwrap()
listener.set_nonblocking(true)?;
Ok(TcpListener::from_std(listener)?)
}
// otherwise fall back to local listening
None => TcpListener::bind(DEFAULT_LISTENER).await.unwrap(),
None => Ok(TcpListener::bind(DEFAULT_LISTENER).await?),
}
}
fn get_livereload_layer(templates_path: &Path) -> LiveReloadLayer<NotHtmxPredicate> {
fn get_livereload_layer(
templates_paths: Vec<PathBuf>,
) -> Result<LiveReloadLayer<NotHtmxPredicate>, notify::Error> {
let livereload = LiveReloadLayer::new();
let reloader = livereload.reloader();
let mut watcher = notify::recommended_watcher(move |_| reloader.reload()).unwrap();
watcher
.watch(templates_path, notify::RecursiveMode::Recursive)
.unwrap();
livereload.request_predicate::<Body, NotHtmxPredicate>(NotHtmxPredicate)
let mut watcher = notify::recommended_watcher(move |_| reloader.reload())?;
for templates_path in templates_paths {
watcher.watch(templates_path.as_path(), notify::RecursiveMode::Recursive)?;
}
Ok(livereload.request_predicate::<Body, NotHtmxPredicate>(NotHtmxPredicate))
}
#[tokio::main]
async fn main() {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
async fn main() -> Result<(), AppError> {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").map_err(|_| AppError::MissingEnvVar {
var: "CARGO_MANIFEST_DIR",
})?;
let assets_path = Path::new(&manifest_dir).join("assets");
let templates_path = Path::new(&manifest_dir).join("templates");
let templates_paths = vec![
Path::new(&manifest_dir).join("src/pages"),
Path::new(&manifest_dir).join("src/components"),
];
let livereload_layer = get_livereload_layer(&templates_path);
let livereload_layer =
get_livereload_layer(templates_paths).map_err(AppError::NotifyWatcher)?;
let router = get_router(assets_path.as_path()).layer(livereload_layer);
let listener: TcpListener = get_tcp_listener().await;
println!("Listening on: http://{}", listener.local_addr().unwrap());
let listener: TcpListener = get_tcp_listener().await.map_err(AppError::TCPListener)?;
let local_addr = listener.local_addr().map_err(AppError::TCPListener)?;
println!("Listening on: http://{}", local_addr);
// Run the server with the router
axum::serve(listener, router.into_make_service())
.await
.unwrap();
axum::serve(listener, router.into_make_service()).await?;
Ok(())
}

23
crates/app/src/menu.rs Normal file
View File

@ -0,0 +1,23 @@
pub struct MenuItem {
pub id: String,
pub label: String,
pub href: String,
}
/// Get the menu items
/// This function is the central place to define the menu items
/// It can be used directly in templates, for example in the `navbar` component to render the menu
pub fn get_menu_items() -> Vec<MenuItem> {
vec![
MenuItem {
id: "home".to_string(),
label: "Accueil".to_string(),
href: "/".to_string(),
},
MenuItem {
id: "cps".to_string(),
label: "CPS".to_string(),
href: "/cps".to_string(),
},
]
}

View File

@ -0,0 +1,43 @@
{% extends "base.html" %}
{% import "navbar/navbar.html" as navbar -%}
{% block title %}Pharma Libre - CPS{% endblock %}
{% block body %}
{% call navbar::navbar(current="cps") %}
<div class="py-10">
<header id="page-header">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<h1
id="page-title"
class="text-3xl font-bold leading-tight tracking-tight text-gray-900"
>
CPS
</h1>
</div>
</header>
<main id="page-main">
<div
class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8"
>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-96 mb-4"
>A</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
<div
class="border-2 border-dashed border-gray-300 rounded-lg dark:border-gray-600 h-32 md:h-64"
>B</div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-32 md:h-64"
>C</div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-32 md:h-64"
>D</div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-32 md:h-64"
>E</div>
</div>
</div>
</main>
</div>
{% endblock %}

View File

@ -1,9 +1,12 @@
use askama_axum::Template;
use axum_htmx::HxRequest;
#[derive(Template)]
#[template(path = "pages/cps.html")]
pub struct CpsTemplate;
pub async fn cps() -> CpsTemplate {
CpsTemplate
#[template(path = "cps.html")]
pub struct CpsTemplate {
hx_request: bool,
}
pub async fn cps(HxRequest(hx_request): HxRequest) -> CpsTemplate {
CpsTemplate { hx_request }
}

View File

@ -0,0 +1,43 @@
{% extends "base.html" %}
{% import "navbar/navbar.html" as navbar -%}
{% block title %}Pharma Libre - Accueil{% endblock %}
{% block body %}
{% call navbar::navbar(current="home") %}
<div class="py-10">
<header id="page-header">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<h1
id="page-title"
class="text-3xl font-bold leading-tight tracking-tight text-gray-900"
>
Accueil
</h1>
</div>
</header>
<main id="page-main">
<div
class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8"
>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
<div
class="border-2 border-dashed border-gray-300 rounded-lg dark:border-gray-600 h-32 md:h-64"
>A</div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-32 md:h-64"
>B</div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-32 md:h-64"
>C</div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-32 md:h-64"
>D</div>
</div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-96 mb-4"
>E</div>
</div>
</main>
</div>
{% endblock %}

View File

@ -1,9 +1,12 @@
use askama_axum::Template;
use axum_htmx::HxRequest;
#[derive(Template)]
#[template(path = "pages/home.html")]
pub struct HomeTemplate;
pub async fn home() -> HomeTemplate {
HomeTemplate
#[template(path = "home.html")]
pub struct GetHomeTemplate {
hx_request: bool,
}
pub async fn home(HxRequest(hx_request): HxRequest) -> GetHomeTemplate {
GetHomeTemplate { hx_request }
}

View File

@ -5,6 +5,6 @@ mod home;
pub fn get_routes() -> Router {
Router::new()
.route("/home", routing::get(home::home))
.route("/", routing::get(home::home))
.route("/cps", routing::get(cps::cps))
}

View File

@ -1,18 +0,0 @@
use askama_axum::Template;
use axum::{routing, Router};
#[derive(Template)]
#[template(path = "hello.html")]
struct HelloTemplate {
pub name: String,
}
async fn hello() -> HelloTemplate {
HelloTemplate {
name: "Theo".to_string(),
}
}
pub fn get_routes() -> Router {
Router::new().route("/", routing::get(hello))
}

View File

@ -1,12 +0,0 @@
use axum::Router;
mod hello;
mod nav;
mod profile;
pub fn get_routes() -> Router {
Router::new()
.nest("/hello", hello::get_routes())
.nest("/nav", nav::get_routes())
.nest("/profile", profile::get_routes())
}

View File

@ -1,60 +0,0 @@
use askama_axum::Template;
use axum::{extract::Query, routing, Router};
use serde::Deserialize;
struct MenuItem {
label: String,
href: String,
current: bool,
}
#[derive(Deserialize)]
struct MenuParameters {
mobile: bool,
}
#[derive(Template)]
#[template(path = "layout/nav/nav-menu-items.html")]
struct MenuTemplate {
mobile: bool,
items: Vec<MenuItem>,
}
impl MenuTemplate {
fn get_classes(&self, is_current_item: &bool) -> String {
let common_classes = match self.mobile {
true => "block border-l-4 py-2 pl-3 pr-4 text-base font-medium".to_string(),
false => {
"inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium".to_string()
}
};
match (self.mobile, is_current_item) {
(true, true) => common_classes + " border-indigo-500 bg-indigo-50 text-indigo-700",
(true, false) => common_classes + " border-transparent text-gray-600 hover:border-gray-300 hover:bg-gray-50 hover:text-gray-800",
(false, true) => common_classes + " border-indigo-500 text-gray-900",
(false, false) => common_classes + " border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
}
}
}
async fn menu(Query(params): Query<MenuParameters>) -> MenuTemplate {
MenuTemplate {
mobile: params.mobile,
items: vec![
MenuItem {
label: "Accueil".to_string(),
href: "/pages/home".to_string(),
current: true,
},
MenuItem {
label: "CPS".to_string(),
href: "/pages/cps".to_string(),
current: false,
},
],
}
}
pub fn get_routes() -> Router {
Router::new().route("/menu", routing::get(menu))
}

View File

@ -1,63 +0,0 @@
use askama_axum::Template;
use axum::{extract::Query, routing, Router};
use serde::Deserialize;
struct MenuItem {
label: String,
id: String,
current: bool,
}
#[derive(Deserialize)]
struct MenuParameters {
mobile: bool,
}
#[derive(Template)]
#[template(path = "layout/nav/profile-menu-items.html")]
struct MenuTemplate {
mobile: bool,
items: Vec<MenuItem>,
}
impl MenuTemplate {
fn get_classes(&self, is_current_item: &bool) -> String {
let common_classes = match self.mobile {
true => "block px-4 py-2 text-base font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-800".to_string(),
false => "block px-4 py-2 text-sm text-gray-700".to_string(),
};
match (self.mobile, is_current_item) {
(true, true) => common_classes + "", // ???
(true, false) => common_classes + "",
(false, true) => common_classes + " bg-gray-100",
(false, false) => common_classes + "",
}
}
}
async fn menu(Query(params): Query<MenuParameters>) -> MenuTemplate {
MenuTemplate {
mobile: params.mobile,
items: vec![
MenuItem {
label: "Votre profil".to_string(),
id: "profile".to_string(),
current: false,
},
MenuItem {
label: "Paramètres".to_string(),
id: "settings".to_string(),
current: false,
},
MenuItem {
label: "Déconnexion".to_string(),
id: "logout".to_string(),
current: false,
},
],
}
}
pub fn get_routes() -> Router {
Router::new().route("/menu", routing::get(menu))
}

View File

@ -1,7 +1,7 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./templates/**/*.html',
'./src/**/*.html',
'./css/**/*.css',
],
theme: {

View File

@ -1 +0,0 @@
<div>Hello {{name}}!</div>

View File

@ -1,34 +0,0 @@
{% extends "base.html" %}
{% block title %}Pharma Libre{% endblock %}
{% block body %}
<div class="py-10">
<header>
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<h1
id="page-title"
class="text-3xl font-bold leading-tight tracking-tight text-gray-900"
>
{% include "skeletons/page-title.html" %}
</h1>
</div>
</header>
<main>
<div
id="main-container"
class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8"
>
<!-- Your content -->
<div
hx-get="/pages/home"
hx-target="this"
hx-trigger="load"
hx-swap="outerHTML"
>
{% include "skeletons/card.html" %}
</div>
</div>
</main>
</div>
{% endblock %}

View File

@ -1,41 +0,0 @@
<nav
class="border-b border-gray-200 bg-white"
x-data="{ menuOpen: false }"
>
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div class="flex h-16 justify-between">
<div class="flex">
{% include "layout/nav/logo.html" %}
<div class="hidden sm:-my-px sm:ml-6 sm:flex sm:space-x-8">
{% include "layout/nav/desktop/menu-items.html" %}
</div>
</div>
<div class="hidden sm:ml-6 sm:flex sm:items-center">
{% include "layout/nav/desktop/notifications-button.html" %}
{% include "layout/nav/desktop/profile.html" %}
</div>
<div class="-mr-2 flex items-center sm:hidden">
{% include "layout/nav/mobile/menu-button.html" %}
</div>
</div>
</div>
<!-- Mobile menu, show/hide based on menu state. -->
<div
class="sm:hidden" id="mobile-menu"
x-show="menuOpen"
x-cloak
>
<div class="space-y-1 pb-3 pt-2">
{% include "layout/nav/mobile/menu-items.html" %}
</div>
<div class="border-t border-gray-200 pb-3 pt-4">
<div class="flex items-center px-4">
{% include "layout/nav/mobile/profile.html" %}
{% include "layout/nav/mobile/notifications-button.html" %}
</div>
<div class="mt-3 space-y-1">
{% include "layout/nav/mobile/profile-items.html" %}
</div>
</div>
</div>
</nav>

View File

@ -1,9 +0,0 @@
<div
id="nav-menu-desktop"
hx-get="/nav/menu?mobile=false"
hx-target="this"
hx-trigger="load"
hx-swap="outerHTML"
>
{% include "skeletons/menu-items.html" %}
</div>

View File

@ -1,6 +0,0 @@
<button
type="button"
class="relative rounded-full bg-white p-1 text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
{% include "layout/nav/notifications-icon.html" %}
</button>

View File

@ -1,22 +0,0 @@
<div
class="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
role="menu"
id="profile-dropdown"
aria-orientation="vertical"
aria-labelledby="user-menu-button"
tabindex="-1"
x-show="profileOpen"
x-on:click.outside="profileOpen = false"
x-cloak
x-transition
>
<div
id="profile-menu-desktop"
hx-get="/profile/menu?mobile=false"
hx-target="this"
hx-trigger="load"
hx-swap="outerHTML"
>
Chargement ...
</div>
</div>

View File

@ -1,27 +0,0 @@
<!-- Profile dropdown -->
<div
class="relative ml-3"
x-data="{ profileOpen: false }"
>
<div>
<button
type="button"
class="relative flex max-w-xs items-center rounded-full bg-white text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
id="user-menu-button"
aria-controls="profile-dropdown"
aria-expanded="false"
aria-haspopup="menu"
x-on:click="profileOpen = ! profileOpen"
>
<span class="absolute -inset-1.5"></span>
<span class="sr-only">Open user menu</span>
<img
class="h-8 w-8 rounded-full"
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
alt=""
/>
</button>
</div>
{% include "layout/nav/desktop/profile-dropdown.html" %}
</div>

View File

@ -1,4 +0,0 @@
<div class="flex flex-shrink-0 items-center">
<img class="block h-8 w-auto lg:hidden" src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600" alt="Your Company">
<img class="hidden h-8 w-auto lg:block" src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600" alt="Your Company">
</div>

View File

@ -1,43 +0,0 @@
<!-- Mobile menu button -->
<button
type="button"
class="relative inline-flex items-center justify-center rounded-md bg-white p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
aria-controls="mobile-menu"
x-on:click="menuOpen = ! menuOpen"
x-bind:aria-expanded="menuOpen"
>
<span class="absolute -inset-0.5"></span>
<span class="sr-only">Open main menu</span>
<!-- Menu open: "hidden", Menu closed: "block" -->
<svg
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
x-bind:class="menuOpen ? 'hidden' : 'block'"
x-bind:aria-hidden="menuOpen"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
/>
</svg>
<!-- Menu open: "block", Menu closed: "hidden" -->
<svg
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
x-bind:class="menuOpen ? 'block' : 'hidden'"
x-bind:aria-hidden="! menuOpen"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>

View File

@ -1,9 +0,0 @@
<div
id="nav-menu-mobile"
hx-get="/nav/menu?mobile=true"
hx-target="this"
hx-trigger="load"
hx-swap="outerHTML"
>
Chargement ...
</div>

View File

@ -1,6 +0,0 @@
<button
type="button"
class="relative ml-auto flex-shrink-0 rounded-full bg-white p-1 text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
{% include "layout/nav/notifications-icon.html" %}
</button>

View File

@ -1,9 +0,0 @@
<div
id="profile-menu-mobile"
hx-get="/profile/menu?mobile=true"
hx-target="this"
hx-trigger="load"
hx-swap="outerHTML"
>
Chargement ...
</div>

View File

@ -1,11 +0,0 @@
<div class="flex-shrink-0">
<img
class="h-10 w-10 rounded-full"
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
alt=""
/>
</div>
<div class="ml-3">
<div class="text-base font-medium text-gray-800">Tom Cook</div>
<div class="text-sm font-medium text-gray-500">tom@example.com</div>
</div>

View File

@ -1,13 +0,0 @@
{% for item in items %}
<a
href=""
hx-get="{{ item.href }}"
hx-trigger="click"
hx-target="#main-container"
hx-swap="innerHTML"
class="{{ Self::get_classes(self, item.current) }}"
aria-current="{% if item.current %}page{% endif %}"
>
{{ item.label }}
</a>
{% endfor %}

View File

@ -1,16 +0,0 @@
<span class="absolute -inset-1.5"></span>
<span class="sr-only">View notifications</span>
<svg
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0"
/>
</svg>

View File

@ -1,11 +0,0 @@
{% for item in items %}
<a
href="#{{ item.id }}"
class="{{ Self::get_classes(self, item.current) }}"
role="menuitem"
tabindex="-1"
id="{{ item.id }}"
>
{{ item.label }}
</a>
{% endfor %}

View File

@ -1,52 +0,0 @@
<h3 id="page-title" hx-swap-oob="textContent">
CPS
</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
<div
class="border-2 border-dashed border-gray-300 rounded-lg dark:border-gray-600 h-32 md:h-64"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-32 md:h-64"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-32 md:h-64"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-32 md:h-64"
></div>
</div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-96 mb-4"
></div>
<div class="grid grid-cols-2 gap-4 mb-4">
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
</div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-96 mb-4"
></div>
<div class="grid grid-cols-2 gap-4">
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
</div>

View File

@ -1,52 +0,0 @@
<h3 id="page-title" hx-swap-oob="textContent">
Accueil
</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
<div
class="border-2 border-dashed border-gray-300 rounded-lg dark:border-gray-600 h-32 md:h-64"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-32 md:h-64"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-32 md:h-64"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-32 md:h-64"
></div>
</div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-96 mb-4"
></div>
<div class="grid grid-cols-2 gap-4 mb-4">
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
</div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-96 mb-4"
></div>
<div class="grid grid-cols-2 gap-4">
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-48 md:h-72"
></div>
</div>

View File

@ -21,4 +21,5 @@ tokio = "1.39.1"
app = { path = "../app" }
http = "1.1.0"
bytes = "1.6.1"
thiserror = "1.0.63"

View File

@ -1,21 +1,30 @@
use axum::body::{to_bytes, Body};
use axum::Router;
use bytes::Bytes;
use http::{request, response, Request, Response};
use std::path::PathBuf;
use std::sync::Arc;
use axum::body::{to_bytes, Body};
use axum::Router;
use tauri::path::BaseDirectory;
use tauri::Manager;
use thiserror::Error;
use tokio::sync::{Mutex, MutexGuard};
use tower::{Service, ServiceExt};
#[derive(Error, Debug)]
pub enum DesktopError {
#[error("Axum error:\n{0}")]
Axum(#[from] axum::Error),
#[error("Infallible error")]
Infallible(#[from] std::convert::Infallible),
}
/// Process requests sent to Tauri (with the `axum://` protocol) and handle them with Axum
/// When an error occurs, this function is expected to panic, which should result in a 500 error
/// being sent to the client, so we let the client handle the error recovering
async fn process_tauri_request(
tauri_request: Request<Vec<u8>>,
mut router: MutexGuard<'_, Router>,
) -> Response<Vec<u8>> {
) -> Result<Response<Vec<u8>>, DesktopError> {
let (parts, body): (request::Parts, Vec<u8>) = tauri_request.into_parts();
let axum_request: Request<Body> = Request::from_parts(parts, body.into());
@ -23,17 +32,16 @@ async fn process_tauri_request(
.as_service()
.ready()
.await
.expect("Failed to get ready service from router")
.map_err(DesktopError::Infallible)?
.call(axum_request)
.await
.expect("Could not get response from router");
.map_err(DesktopError::Infallible)?;
let (parts, body): (response::Parts, Body) = axum_response.into_parts();
let body: Bytes = to_bytes(body, usize::MAX).await.unwrap_or_default();
let body: Bytes = to_bytes(body, usize::MAX).await?;
let tauri_response: Response<Vec<u8>> = Response::from_parts(parts, body.into());
tauri_response
Ok(tauri_response)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
@ -43,7 +51,7 @@ pub fn run() {
let assets_path: PathBuf = app
.path()
.resolve("assets", BaseDirectory::Resource)
.expect("Path should be resolvable");
.expect("Assets path should be resolvable");
// Adds Axum router to application state
// This makes it so we can retrieve it from any app instance (see bellow)
@ -60,8 +68,19 @@ pub fn run() {
// Spawn a new async task to process the request
tauri::async_runtime::spawn(async move {
let router = router.lock().await;
let response = process_tauri_request(request, router).await;
responder.respond(response);
match process_tauri_request(request, router).await {
Ok(response) => responder.respond(response),
Err(err) => {
let body = format!("Failed to process an axum:// request:\n{}", err);
responder.respond(
http::Response::builder()
.status(http::StatusCode::BAD_REQUEST)
.header(http::header::CONTENT_TYPE, "text/plain")
.body::<Vec<u8>>(body.into())
.expect("BAD_REQUEST response should be valid"),
)
}
}
});
})
.run(tauri::generate_context!())

View File

@ -1,3 +0,0 @@
SESAM_FSV_VERSION=1.40.14
SESAM_FSV_LIB_PATH="C:/Program Files/santesocial/fsv/${SESAM_FSV_VERSION}/lib"
SESAM_FSV_SSVLIB=ssvw64

View File

@ -1,2 +0,0 @@
SESAM_FSV_VERSION=1.40.14
SESAM_INI_PATH=${ALLUSERSPROFILE}\\santesocial\\fsv\\${SESAM_FSV_VERSION}\\conf\\sesam.ini

View File

@ -4,8 +4,10 @@ version = "0.1.0"
edition = "2021"
[dependencies]
dotenv = "0.15"
anyhow = "1.0"
libc = "0.2"
thiserror = "1.0"
utils = { path = "../utils" }
[build-dependencies]
dotenv = "0.15"

View File

@ -1,13 +1,13 @@
extern crate dotenv;
use std::env;
use std::path::PathBuf;
use dotenv::from_path;
fn main() {
// Load the .env.build file for build-time environment variables
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set");
let manifest_path = PathBuf::from(manifest_dir);
dotenv::from_path(manifest_path.join(".env.build")).ok();
from_path(manifest_path.join(".env.build")).ok();
println!("cargo::rerun-if-env-changed=SESAM_FSV_LIB_PATH");
println!("cargo::rerun-if-env-changed=SESAM_FSV_SSVLIB");
@ -22,12 +22,13 @@ fn main() {
);
// Add the SESAM_FSV_LIB_PATH to the linker search path
let fsv_lib_path = PathBuf::from(env::var("SESAM_FSV_LIB_PATH").unwrap());
let fsv_lib_path =
PathBuf::from(env::var("SESAM_FSV_LIB_PATH").expect("SESAM_FSV_LIB_PATH must be set"));
println!("cargo::rustc-link-search=native={}", fsv_lib_path.display());
// Add the SESAM_FSV_LIB_PATH to the PATH environment variable
if cfg!(target_os = "windows") {
let path = env::var("PATH").unwrap_or(String::new());
let path = env::var("PATH").unwrap_or_default();
println!("cargo:rustc-env=PATH={};{}", fsv_lib_path.display(), path);
} else if cfg!(target_os = "linux") {
println!("cargo:rustc-env=LD_LIBRARY_PATH={}", fsv_lib_path.display());
@ -36,7 +37,7 @@ fn main() {
// Link the SESAM_FSV_SSVLIB dynamic library
println!(
"cargo::rustc-link-lib=dylib={}",
env::var("SESAM_FSV_SSVLIB").unwrap()
env::var("SESAM_FSV_SSVLIB").expect("SESAM_FSV_SSVLIB must be set")
);
// TODO : try `raw-dylib` instead of `dylib` on Windows to avoid the need of the `lib` headers compiled from the `def`
}

Binary file not shown.

View File

@ -1,876 +0,0 @@
use libc::{c_void, size_t};
use std::ffi::CString;
use std::ptr;
use crate::libssv::SSV_LireDroitsVitale;
use crate::ssv_memory::{decode_ssv_memory, Block};
#[derive(Debug, Default)]
pub struct CarteVitale {
donneesAssure: DonneesAssure,
serviceAMOFamille: ServiceAMOFamille,
donneesAccidentTravail: DonneesAccidentTravail,
donneesBeneficiaire: Vec<DonneesBeneficiaire>,
}
// 1. CB = Caractères Binaires »
// 2. CE = Caractères « Etendus » (ISO 8859-1)
// 3. CA = Caractères Alphanumériques (ASCII?)
// 4. CN = Caractères Numériques
#[derive(Debug, Default)]
struct DonneesAssure {
type_de_carte_vitale: char, // CA
numero_de_serie: String, // CN - 1 -> 20
date_de_fin_de_validite: String, // CN - Format AAAAMMJJ0000
donnees_administration_ruf1: char, // CN
donnees_administration_ruf2: String, // CA - 24
donnees_ruf_administration: String, // CE - 10
type_d_identification_du_porteur: char, // CA
numero_national_d_immatriculation: String, // CA - 13
cle_du_nir: String, // CA - 2
code_regime: String, // CN - 2
caisse_gestionnaire: String, // CN - 3
centre_gestionnaire: String, // CN - 4
code_gestion: String, // CA - 2
donnees_ruf_famille: String, // CE - 55
}
#[derive(Debug, Default)]
struct ServiceAMOFamille {
code_service_amo_famille: String, // CN - 2
date_de_debut_service_amo_famille: String, // CN - Format AAAAMMJJ0000
date_de_fin_service_amo_famille: String, // CN - Format AAAAMMJJ0000
}
#[derive(Debug, Default)]
struct DonneesAccidentTravail {
organisme_gestionnaire_risque_at: String, // CN - 9
code_at: char, // CA - 2
identifiant_at: String, // CN - 9
organisme_gestionnaire_at1: String, // CN - 9
code_at1: char, // CA - 2
identifiant_at1: String, // CN - 9
organisme_gestionnaire_at2: String, // CN - 9
code_at2: char, // CA - 2
identifiant_at2: String, // CN - 9
}
#[derive(Debug, Default)]
struct DonneesBeneficiaire {
nom_usuel: String, // CE - 27
nom_de_famille: String, // CE - 27
prenom: String, // CE - 27
adresse_ligne_1: String, // CE - 32
adresse_ligne_2: String, // CE - 32
adresse_ligne_3: String, // CE - 32
adresse_ligne_4: String, // CE - 32
adresse_ligne_5: String, // CE - 32
nir_certifie: String, // CA - 13
cle_du_nir_certifie: String, // CN - 2
date_de_certification_du_nir: String, // CN - Format AAAAMMJJ0000
date_de_naissance: String, // CN - Format AAAAMMJJ0000
rang_de_naissance: String, // CN - 1
qualite: String, // CN - 2
code_service_amo_beneficiaire: char, // CA - 2
date_de_debut_service_amo: String, // CN - Format AAAAMMJJ0000
date_de_fin_service_amo: String, // CN - Format AAAAMMJJ0000
donnees_ruf_amo: String, // CE - 30
periodeDroitsAMO: Vec<PeriodeDroitsAMO>,
periodeCodeCouverture: Vec<PeriodeCodeCouverture>,
donneesMutuelle: DonneesMutuelle,
donneesComplementaire: DonneesComplementaire,
donneesRUFBeneficiaireComplementaire: DonneesRUFBeneficiaireComplementaire,
}
#[derive(Debug, Default)]
struct PeriodeDroitsAMO {
date_de_debut_droits_amo: String, // CN - Format AAAAMMJJ0000
date_de_fin_droits_amo: String, // CN - Format AAAAMMJJ0000
}
#[derive(Debug, Default)]
struct PeriodeCodeCouverture {
date_de_debut_code_couverture: String, // CN - Format AAAAMMJJ0000
date_de_fin_code_couverture: String, // CN - Format AAAAMMJJ0000
code_ald: String, // CN - 1
code_situation: String, // CN - 4
}
#[derive(Debug, Default)]
struct DonneesMutuelle {
identification_mutuelle: String, // CN - 8
garanties_effectives: String, // CA - 8
indicateur_de_traitement_mutuelle: String, // CN - 2
type_de_services_associes: char, // CA - 1
services_associes_au_contrat: String, // CA - 17
code_aiguillage_sts: char, // CA - 1
periodeDroitsMutuelle: Vec<PeriodeDroitsMutuelle>,
}
#[derive(Debug, Default)]
struct PeriodeDroitsMutuelle {
date_de_debut_droits_mutuelle: String, // CN - Format AAAAMMJJ0000
date_de_fin_droits_mutuelle: String, // CN - Format AAAAMMJJ0000
}
#[derive(Debug, Default)]
struct DonneesComplementaire {
numero_complementaire_b2: String, // CA - 10
numero_complementaire_edi: String, // CA - 19
numero_adherent_amc: String, // CA - 8
indicateur_de_traitement_amc: String, // CN - 2
date_de_debut_validite_amc: String, // CN - Format AAAAMMJJ0000
date_de_fin_validite_amc: String, // CN - Format AAAAMMJJ0000
code_routage_flux_amc: String, // CA - 2
identifiant_hote: String, // CA - 3
nom_de_domaine_amc: String, // CE - 20
code_aiguillage_sts: char, // CA - 1
type_de_services_associes: char, // CA - 1
services_associes_au_contrat: String, // CE - 17
}
#[derive(Debug, Default)]
struct DonneesRUFBeneficiaireComplementaire {
donnees_ruf_beneficiaire_complementaire: String, // CE - 115
}
pub fn LireDroitsVitale(
lecteurPS: &str,
lecteurVitale: &str,
codePorteurPS: &str,
dateConsultation: &str,
) -> Result<CarteVitale, String> {
let resource_ps = CString::new(lecteurPS).expect("CString::new failed");
let resource_vitale = CString::new(lecteurVitale).expect("CString::new failed");
let card_number = CString::new(codePorteurPS).expect("CString::new failed");
let date_consultation = CString::new(dateConsultation).expect("CString::new failed");
let mut buffer: *mut c_void = ptr::null_mut();
let mut size: size_t = 0;
let mut hex_values: &[u8] = &[];
unsafe {
let result = SSV_LireDroitsVitale(
resource_ps.as_ptr(),
resource_vitale.as_ptr(),
card_number.as_ptr(),
date_consultation.as_ptr(),
&mut buffer,
&mut size,
);
println!("SSV_LireDroitsVitale result: {}", result);
if !buffer.is_null() {
hex_values = std::slice::from_raw_parts(buffer as *const u8, size);
}
}
let groups = decode_ssv_memory(hex_values, hex_values.len());
let OUT = decode_carte_vitale(groups);
unsafe {
if !buffer.is_null() {
libc::free(buffer); // ??? si liberer avant decode_ssv_memory et decode_carte_vitale alors hex_values n'est plus correct ???
}
}
OUT
}
fn decode_carte_vitale(groups: Vec<Block>) -> Result<CarteVitale, String> {
let mut cartevitale = CarteVitale::default();
for group in groups {
for field in group.content {
match (group.id, field.id) {
(104, 1) => {
cartevitale
.donneesBeneficiaire
.push(DonneesBeneficiaire::default());
}
(105, 1) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.periodeDroitsAMO
.push(PeriodeDroitsAMO::default());
}
(106, 1) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.periodeCodeCouverture
.push(PeriodeCodeCouverture::default());
}
(108, 1) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.donneesMutuelle
.periodeDroitsMutuelle
.push(PeriodeDroitsMutuelle::default());
}
_ => {}
}
if field.content.len() > 0 {
match (group.id, field.id) {
(101, 1) => {
let byte = field.content[0];
cartevitale.donneesAssure.type_de_carte_vitale = byte as char;
}
(101, 2) => {
cartevitale.donneesAssure.numero_de_serie = //bytes_to_decimal_string(&field.content);
String::from_utf8_lossy(field.content).to_string();
}
(101, 3) => {
cartevitale.donneesAssure.date_de_fin_de_validite =
String::from_utf8_lossy(field.content).to_string();
}
(101, 4) => {
println!("101.4 field.size: {:#?}", field.size);
let byte = field.content[0];
cartevitale.donneesAssure.donnees_administration_ruf1 = byte as char;
}
(101, 5) => {
cartevitale.donneesAssure.donnees_administration_ruf2 =
String::from_utf8_lossy(field.content).to_string();
}
(101, 6) => {
cartevitale.donneesAssure.donnees_ruf_administration =
String::from_utf8_lossy(field.content).to_string();
}
(101, 7) => {
let byte = field.content[0];
cartevitale.donneesAssure.type_d_identification_du_porteur = byte as char;
}
(101, 8) => {
cartevitale.donneesAssure.numero_national_d_immatriculation =
String::from_utf8_lossy(field.content).to_string();
}
(101, 9) => {
cartevitale.donneesAssure.cle_du_nir =
String::from_utf8_lossy(field.content).to_string();
}
(101, 10) => {
cartevitale.donneesAssure.code_regime =
String::from_utf8_lossy(field.content).to_string();
}
(101, 11) => {
cartevitale.donneesAssure.caisse_gestionnaire =
String::from_utf8_lossy(field.content).to_string();
}
(101, 12) => {
cartevitale.donneesAssure.centre_gestionnaire =
String::from_utf8_lossy(field.content).to_string();
}
(101, 13) => {
cartevitale.donneesAssure.code_gestion =
String::from_utf8_lossy(field.content).to_string();
}
(101, 14) => {
cartevitale.donneesAssure.donnees_ruf_famille =
String::from_utf8_lossy(field.content).to_string();
}
(102, 1) => {
cartevitale.serviceAMOFamille.code_service_amo_famille =
String::from_utf8_lossy(field.content).to_string();
}
(102, 2) => {
cartevitale
.serviceAMOFamille
.date_de_debut_service_amo_famille =
String::from_utf8_lossy(field.content).to_string();
}
(102, 3) => {
cartevitale
.serviceAMOFamille
.date_de_fin_service_amo_famille =
String::from_utf8_lossy(field.content).to_string();
}
(103, 1) => {
cartevitale
.donneesAccidentTravail
.organisme_gestionnaire_risque_at =
String::from_utf8_lossy(field.content).to_string();
}
(103, 2) => {
let byte = field.content[0];
cartevitale.donneesAccidentTravail.code_at = byte as char;
}
(103, 3) => {
cartevitale.donneesAccidentTravail.identifiant_at =
String::from_utf8_lossy(field.content).to_string();
}
(103, 4) => {
cartevitale
.donneesAccidentTravail
.organisme_gestionnaire_at1 =
String::from_utf8_lossy(field.content).to_string();
}
(103, 5) => {
let byte = field.content[0];
cartevitale.donneesAccidentTravail.code_at1 = byte as char;
}
(103, 6) => {
cartevitale.donneesAccidentTravail.identifiant_at1 =
String::from_utf8_lossy(field.content).to_string();
}
(103, 7) => {
cartevitale
.donneesAccidentTravail
.organisme_gestionnaire_at2 =
String::from_utf8_lossy(field.content).to_string();
}
(103, 8) => {
let byte = field.content[0];
cartevitale.donneesAccidentTravail.code_at2 = byte as char;
}
(103, 9) => {
cartevitale.donneesAccidentTravail.identifiant_at2 =
String::from_utf8_lossy(field.content).to_string();
}
(104, 1) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.nom_usuel = String::from_utf8_lossy(field.content).to_string();
}
(104, 2) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.nom_de_famille = String::from_utf8_lossy(field.content).to_string();
}
(104, 3) => {
cartevitale.donneesBeneficiaire.last_mut().unwrap().prenom =
String::from_utf8_lossy(field.content).to_string();
}
(104, 4) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.adresse_ligne_1 = String::from_utf8_lossy(field.content).to_string();
}
(104, 5) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.adresse_ligne_2 = String::from_utf8_lossy(field.content).to_string();
}
(104, 6) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.adresse_ligne_3 = String::from_utf8_lossy(field.content).to_string();
}
(104, 7) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.adresse_ligne_4 = String::from_utf8_lossy(field.content).to_string();
}
(104, 8) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.adresse_ligne_5 = String::from_utf8_lossy(field.content).to_string();
}
(104, 9) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.nir_certifie = String::from_utf8_lossy(field.content).to_string();
}
(104, 10) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.cle_du_nir_certifie =
String::from_utf8_lossy(field.content).to_string();
}
(104, 11) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.date_de_certification_du_nir =
String::from_utf8_lossy(field.content).to_string();
}
(104, 12) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.date_de_naissance = String::from_utf8_lossy(field.content).to_string();
}
(104, 13) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.rang_de_naissance = String::from_utf8_lossy(field.content).to_string();
}
(104, 14) => {
cartevitale.donneesBeneficiaire.last_mut().unwrap().qualite =
String::from_utf8_lossy(field.content).to_string();
}
(104, 15) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.code_service_amo_beneficiaire = field.content[0] as char;
}
(104, 16) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.date_de_debut_service_amo =
String::from_utf8_lossy(field.content).to_string();
}
(104, 17) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.date_de_fin_service_amo =
String::from_utf8_lossy(field.content).to_string();
}
(104, 18) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.donnees_ruf_amo = String::from_utf8_lossy(field.content).to_string();
}
(105, 1) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.periodeDroitsAMO
.last_mut()
.unwrap()
.date_de_debut_droits_amo =
String::from_utf8_lossy(field.content).to_string();
}
(105, 2) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.periodeDroitsAMO
.last_mut()
.unwrap()
.date_de_fin_droits_amo =
String::from_utf8_lossy(field.content).to_string();
}
(106, 1) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.periodeCodeCouverture
.last_mut()
.unwrap()
.date_de_debut_code_couverture =
String::from_utf8_lossy(field.content).to_string();
}
(106, 2) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.periodeCodeCouverture
.last_mut()
.unwrap()
.date_de_fin_code_couverture =
String::from_utf8_lossy(field.content).to_string();
}
(106, 3) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.periodeCodeCouverture
.last_mut()
.unwrap()
.code_ald = String::from_utf8_lossy(field.content).to_string();
}
(106, 4) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.periodeCodeCouverture
.last_mut()
.unwrap()
.code_situation = String::from_utf8_lossy(field.content).to_string();
}
(107, 1) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.donneesMutuelle
.identification_mutuelle =
String::from_utf8_lossy(field.content).to_string();
}
(107, 2) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.donneesMutuelle
.garanties_effectives =
String::from_utf8_lossy(field.content).to_string();
}
(107, 3) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.donneesMutuelle
.indicateur_de_traitement_mutuelle =
String::from_utf8_lossy(field.content).to_string();
}
(107, 4) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.donneesMutuelle
.type_de_services_associes = field.content[0] as char;
}
(107, 5) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.donneesMutuelle
.services_associes_au_contrat =
String::from_utf8_lossy(field.content).to_string();
}
(107, 6) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.donneesMutuelle
.code_aiguillage_sts = field.content[0] as char;
}
(108, 1) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.donneesMutuelle
.periodeDroitsMutuelle
.last_mut()
.unwrap()
.date_de_debut_droits_mutuelle =
String::from_utf8_lossy(field.content).to_string();
}
(108, 2) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.donneesMutuelle
.periodeDroitsMutuelle
.last_mut()
.unwrap()
.date_de_fin_droits_mutuelle =
String::from_utf8_lossy(field.content).to_string();
}
(109, 1) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.donneesComplementaire
.numero_complementaire_b2 =
String::from_utf8_lossy(field.content).to_string();
}
(109, 2) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.donneesComplementaire
.numero_complementaire_edi =
String::from_utf8_lossy(field.content).to_string();
}
(109, 3) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.donneesComplementaire
.numero_adherent_amc =
String::from_utf8_lossy(field.content).to_string();
}
(109, 4) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.donneesComplementaire
.indicateur_de_traitement_amc =
String::from_utf8_lossy(field.content).to_string();
}
(109, 5) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.donneesComplementaire
.date_de_debut_validite_amc =
String::from_utf8_lossy(field.content).to_string();
}
(109, 6) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.donneesComplementaire
.date_de_fin_validite_amc =
String::from_utf8_lossy(field.content).to_string();
}
(109, 7) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.donneesComplementaire
.code_routage_flux_amc =
String::from_utf8_lossy(field.content).to_string();
}
(109, 8) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.donneesComplementaire
.identifiant_hote = String::from_utf8_lossy(field.content).to_string();
}
(109, 9) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.donneesComplementaire
.nom_de_domaine_amc =
String::from_utf8_lossy(field.content).to_string();
}
(109, 10) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.donneesComplementaire
.code_aiguillage_sts = field.content[0] as char;
}
(109, 11) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.donneesComplementaire
.type_de_services_associes = field.content[0] as char;
}
(109, 12) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.donneesComplementaire
.services_associes_au_contrat =
String::from_utf8_lossy(field.content).to_string();
}
(111, 1) => {
cartevitale
.donneesBeneficiaire
.last_mut()
.unwrap()
.donneesRUFBeneficiaireComplementaire
.donnees_ruf_beneficiaire_complementaire =
String::from_utf8_lossy(field.content).to_string();
}
_ => {
return Err(format!(
"Unknown (group, field) pair: ({}, {})",
group.id, field.id
))
}
}
}
}
}
Ok(cartevitale)
}
// #[cfg(test)]
// mod test_decode_carte_ps {
// use super::*;
// #[test]
// fn test_francoise_pharmacien0052419() {
// let bytes: &[u8] = &[
// 0, 1, 51, // Block 01, Content size 51
// 1, 48, // Field 01, Content size 1
// 1, 56, // Field 02, Content size 1
// 11, 57, 57, 55, 48, 48, 53, 50, 52, 49, 57, 52, // Field 03, Content size 11
// 1, 52, // Field 04, Content size 1
// 2, 50, 50, // Field 05, Content size 2
// 17, 80, 72, 65, 82, 77, 65, 67, 73, 69, 78, 48, 48, 53, 50, 52, 49,
// 57, // Field 06, Content size 17
// 9, 70, 82, 65, 78, 67, 79, 73, 83, 69, // Field 07, Content size 9
// 1, 84, // Field 08, Content size 1
// 0, 2, 83, // Block 02, Content size 83
// 1, 1, 1, 48, 1, 49, 2, 56, 54, 1, 49, 9, 48, 66, 48, 50, 50, 49, 57, 53, 56, 1, 56, 24,
// 80, 72, 65, 82, 77, 65, 67, 73, 69, 32, 68, 85, 32, 67, 69, 78, 84, 82, 69, 50, 50, 49,
// 57, 53, 8, 48, 48, 50, 48, 50, 52, 49, 57, 1, 56, 0, 1, 48, 1, 49, 2, 53, 48, 2, 49,
// 48, 2, 48, 48, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49,
// ];
// let blocks = decode_ssv_memory(bytes, bytes.len());
// let carte_ps = decode_carte_ps(blocks).unwrap();
// assert_eq!(carte_ps.titulaire.type_de_carte_ps, "0");
// assert_eq!(carte_ps.titulaire.type_d_identification_nationale, "8");
// assert_eq!(
// carte_ps.titulaire.numero_d_identification_nationale,
// "99700524194"
// );
// assert_eq!(
// carte_ps.titulaire.cle_du_numero_d_identification_nationale,
// "4"
// );
// assert_eq!(carte_ps.titulaire.code_civilite, "22");
// assert_eq!(carte_ps.titulaire.nom_du_ps, "PHARMACIEN0052419");
// assert_eq!(carte_ps.titulaire.prenom_du_ps, "FRANCOISE");
// assert_eq!(carte_ps.titulaire.categorie_carte, 'T');
// assert_eq!(carte_ps.situations.len(), 1);
// assert_eq!(
// carte_ps.situations[0].numero_logique_de_la_situation_de_facturation_du_ps,
// 1
// );
// assert_eq!(carte_ps.situations[0].mode_d_exercice, "0");
// assert_eq!(carte_ps.situations[0].statut_d_exercice, "1");
// assert_eq!(carte_ps.situations[0].secteur_d_activite, "86");
// assert_eq!(carte_ps.situations[0].type_d_identification_structure, "1");
// assert_eq!(
// carte_ps.situations[0].numero_d_identification_structure,
// "0B0221958"
// );
// assert_eq!(
// carte_ps.situations[0].cle_du_numero_d_identification_structure,
// "8"
// );
// assert_eq!(
// carte_ps.situations[0].raison_sociale_structure,
// "PHARMACIE DU CENTRE22195"
// );
// assert_eq!(
// carte_ps.situations[0].numero_d_identification_de_facturation_du_ps,
// "00202419"
// );
// assert_eq!(
// carte_ps.situations[0].cle_du_numero_d_identification_de_facturation_du_ps,
// "8"
// );
// assert_eq!(
// carte_ps.situations[0].numero_d_identification_du_ps_remplaçant,
// ""
// );
// assert_eq!(
// carte_ps.situations[0].cle_du_numero_d_identification_du_ps_remplaçant,
// "0"
// );
// assert_eq!(carte_ps.situations[0].code_conventionnel, "1");
// assert_eq!(carte_ps.situations[0].code_specialite, "50");
// assert_eq!(carte_ps.situations[0].code_zone_tarifaire, "10");
// assert_eq!(carte_ps.situations[0].code_zone_ik, "00");
// assert_eq!(carte_ps.situations[0].code_agrement_1, "0");
// assert_eq!(carte_ps.situations[0].code_agrement_2, "0");
// assert_eq!(carte_ps.situations[0].code_agrement_3, "0");
// assert_eq!(
// carte_ps.situations[0].habilitation_à_signer_une_facture,
// "1"
// );
// assert_eq!(carte_ps.situations[0].habilitation_à_signer_un_lot, "1");
// }
// #[test]
// fn test_multiple_situations() {
// let bytes: &[u8] = &[
// 0, 1, 51, // Block 01, Content size 51
// 1, 48, 1, 56, 11, 57, 57, 55, 48, 48, 53, 50, 52, 49, 57, 52, 1, 52, 2, 50, 50, 17, 80,
// 72, 65, 82, 77, 65, 67, 73, 69, 78, 48, 48, 53, 50, 52, 49, 57, 9, 70, 82, 65, 78, 67,
// 79, 73, 83, 69, 1, 84, 0, 2, 83, // Block 02, Content size 83
// 1, 1, 1, 48, 1, 49, 2, 56, 54, 1, 49, 9, 48, 66, 48, 50, 50, 49, 57, 53, 56, 1, 56, 24,
// 80, 72, 65, 82, 77, 65, 67, 73, 69, 32, 68, 85, 32, 67, 69, 78, 84, 82, 69, 50, 50, 49,
// 57, 53, 8, 48, 48, 50, 48, 50, 52, 49, 57, 1, 56, 0, 1, 48, 1, 49, 2, 53, 48, 2, 49,
// 48, 2, 48, 48, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49, 0, 3,
// 83, // Block 03, Content size 83
// 1, 1, 1, 48, 1, 49, 2, 56, 54, 1, 49, 9, 48, 66, 48, 50, 50, 49, 57, 53, 56, 1, 56, 24,
// 80, 72, 65, 82, 77, 65, 67, 73, 69, 32, 68, 85, 32, 67, 69, 78, 84, 82, 69, 50, 50, 49,
// 57, 53, 8, 48, 48, 50, 48, 50, 52, 49, 57, 1, 56, 0, 1, 48, 1, 49, 2, 53, 48, 2, 49,
// 48, 2, 48, 48, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49, 0, 4,
// 83, // Block 04, Content size 83
// 1, 1, 1, 48, 1, 49, 2, 56, 54, 1, 49, 9, 48, 66, 48, 50, 50, 49, 57, 53, 56, 1, 56, 24,
// 80, 72, 65, 82, 77, 65, 67, 73, 69, 32, 68, 85, 32, 67, 69, 78, 84, 82, 69, 50, 50, 49,
// 57, 53, 8, 48, 48, 50, 48, 50, 52, 49, 57, 1, 56, 0, 1, 48, 1, 49, 2, 53, 48, 2, 49,
// 48, 2, 48, 48, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49,
// ];
// let blocks = decode_ssv_memory(bytes, bytes.len());
// let carte_ps = decode_carte_ps(blocks).unwrap();
// assert_eq!(carte_ps.situations.len(), 3);
// assert_eq!(
// carte_ps.situations[0].raison_sociale_structure,
// "PHARMACIE DU CENTRE22195"
// );
// assert_eq!(
// carte_ps.situations[1].raison_sociale_structure,
// "PHARMACIE DU CENTRE22195"
// );
// assert_eq!(
// carte_ps.situations[2].raison_sociale_structure,
// "PHARMACIE DU CENTRE22195"
// );
// }
// #[test]
// #[should_panic]
// fn test_missing_field() {
// todo!();
// }
// #[test]
// #[should_panic]
// fn test_unknown_group_field_pair() {
// todo!();
// }
// #[test]
// #[should_panic]
// fn test_invalid_field_format() {
// todo!();
// }
// }

View File

@ -1,9 +1,24 @@
use libc::{c_void, size_t};
use std::ffi::CString;
use std::ptr;
use thiserror::Error;
use crate::libssv::SSV_LireCartePS;
use crate::ssv_memory::{decode_ssv_memory, Block};
use crate::libssv::{self, SSV_LireCartePS};
use crate::ssv_memory::{decode_ssv_memory, Block, SSVMemoryError};
#[derive(Error, Debug)]
pub enum CartePSError {
#[error("Unknown (group, field) pair: ({group}, {field})")]
UnknownGroupFieldPair { group: u16, field: u16 },
#[error("CString creation error: {0}")]
CString(#[from] std::ffi::NulError),
#[error("Unable to get the last situation while parsing a CartePS")]
InvalidLastSituation,
#[error(transparent)]
SSVMemory(#[from] SSVMemoryError),
#[error(transparent)]
SSVLibErrorCode(#[from] libssv::LibSSVError),
}
#[derive(Debug, Default)]
pub struct CartePS {
@ -52,10 +67,10 @@ struct SituationPS {
habilitation_à_signer_un_lot: String,
}
pub fn lire_carte(code_pin: &str, lecteur: &str) -> Result<CartePS, String> {
let resource_ps = CString::new(lecteur).expect("CString::new failed");
let resource_reader = CString::new("").expect("CString::new failed");
let card_number = CString::new(code_pin).expect("CString::new failed");
pub fn lire_carte(code_pin: &str, lecteur: &str) -> Result<CartePS, CartePSError> {
let resource_ps = CString::new(lecteur)?;
let resource_reader = CString::new("")?;
let card_number = CString::new(code_pin)?;
let mut buffer: *mut c_void = ptr::null_mut();
let mut size: size_t = 0;
@ -69,17 +84,32 @@ pub fn lire_carte(code_pin: &str, lecteur: &str) -> Result<CartePS, String> {
&mut size,
);
println!("SSV_LireCartePS result: {}", result);
if result != 0 {
return Err(libssv::LibSSVError::StandardErrorCode {
code: result,
function: "SSV_LireCartePS",
}
.into());
}
if !buffer.is_null() {
hex_values = std::slice::from_raw_parts(buffer as *const u8, size);
libc::free(buffer);
}
}
let groups = decode_ssv_memory(hex_values, hex_values.len());
let groups =
decode_ssv_memory(hex_values, hex_values.len()).map_err(CartePSError::SSVMemory)?;
decode_carte_ps(groups)
}
fn decode_carte_ps(groups: Vec<Block>) -> Result<CartePS, String> {
fn get_last_mut_situation(carte_ps: &mut CartePS) -> Result<&mut SituationPS, CartePSError> {
carte_ps
.situations
.last_mut()
.ok_or(CartePSError::InvalidLastSituation)
}
fn decode_carte_ps(groups: Vec<Block>) -> Result<CartePS, CartePSError> {
let mut carte_ps = CartePS::default();
for group in groups {
for field in group.content {
@ -118,137 +148,99 @@ fn decode_carte_ps(groups: Vec<Block>) -> Result<CartePS, String> {
}
(2..=16, 1) => {
carte_ps.situations.push(SituationPS::default());
carte_ps
.situations
.last_mut()
.unwrap()
get_last_mut_situation(&mut carte_ps)?
.numero_logique_de_la_situation_de_facturation_du_ps = field.content[0];
}
(2..=16, 2) => {
carte_ps.situations.last_mut().unwrap().mode_d_exercice =
get_last_mut_situation(&mut carte_ps)?.mode_d_exercice =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 3) => {
carte_ps.situations.last_mut().unwrap().statut_d_exercice =
get_last_mut_situation(&mut carte_ps)?.statut_d_exercice =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 4) => {
carte_ps.situations.last_mut().unwrap().secteur_d_activite =
get_last_mut_situation(&mut carte_ps)?.secteur_d_activite =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 5) => {
carte_ps
.situations
.last_mut()
.unwrap()
.type_d_identification_structure =
get_last_mut_situation(&mut carte_ps)?.type_d_identification_structure =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 6) => {
carte_ps
.situations
.last_mut()
.unwrap()
.numero_d_identification_structure =
get_last_mut_situation(&mut carte_ps)?.numero_d_identification_structure =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 7) => {
carte_ps
.situations
.last_mut()
.unwrap()
get_last_mut_situation(&mut carte_ps)?
.cle_du_numero_d_identification_structure =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 8) => {
carte_ps
.situations
.last_mut()
.unwrap()
.raison_sociale_structure =
get_last_mut_situation(&mut carte_ps)?.raison_sociale_structure =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 9) => {
carte_ps
.situations
.last_mut()
.unwrap()
get_last_mut_situation(&mut carte_ps)?
.numero_d_identification_de_facturation_du_ps =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 10) => {
carte_ps
.situations
.last_mut()
.unwrap()
get_last_mut_situation(&mut carte_ps)?
.cle_du_numero_d_identification_de_facturation_du_ps =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 11) => {
carte_ps
.situations
.last_mut()
.unwrap()
get_last_mut_situation(&mut carte_ps)?
.numero_d_identification_du_ps_remplaçant =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 12) => {
carte_ps
.situations
.last_mut()
.unwrap()
get_last_mut_situation(&mut carte_ps)?
.cle_du_numero_d_identification_du_ps_remplaçant =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 13) => {
carte_ps.situations.last_mut().unwrap().code_conventionnel =
get_last_mut_situation(&mut carte_ps)?.code_conventionnel =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 14) => {
carte_ps.situations.last_mut().unwrap().code_specialite =
get_last_mut_situation(&mut carte_ps)?.code_specialite =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 15) => {
carte_ps.situations.last_mut().unwrap().code_zone_tarifaire =
get_last_mut_situation(&mut carte_ps)?.code_zone_tarifaire =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 16) => {
carte_ps.situations.last_mut().unwrap().code_zone_ik =
get_last_mut_situation(&mut carte_ps)?.code_zone_ik =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 17) => {
carte_ps.situations.last_mut().unwrap().code_agrement_1 =
get_last_mut_situation(&mut carte_ps)?.code_agrement_1 =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 18) => {
carte_ps.situations.last_mut().unwrap().code_agrement_2 =
get_last_mut_situation(&mut carte_ps)?.code_agrement_2 =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 19) => {
carte_ps.situations.last_mut().unwrap().code_agrement_3 =
get_last_mut_situation(&mut carte_ps)?.code_agrement_3 =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 20) => {
carte_ps
.situations
.last_mut()
.unwrap()
.habilitation_à_signer_une_facture =
get_last_mut_situation(&mut carte_ps)?.habilitation_à_signer_une_facture =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 21) => {
carte_ps
.situations
.last_mut()
.unwrap()
.habilitation_à_signer_un_lot =
get_last_mut_situation(&mut carte_ps)?.habilitation_à_signer_un_lot =
String::from_utf8_lossy(field.content).to_string();
}
_ => {
return Err(format!(
"Unknown (group, field) pair: ({}, {})",
group.id, field.id
))
return Err(CartePSError::UnknownGroupFieldPair {
group: group.id,
field: field.id,
});
}
}
}
@ -279,7 +271,7 @@ mod test_decode_carte_ps {
57, 53, 8, 48, 48, 50, 48, 50, 52, 49, 57, 1, 56, 0, 1, 48, 1, 49, 2, 53, 48, 2, 49,
48, 2, 48, 48, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49,
];
let blocks = decode_ssv_memory(bytes, bytes.len());
let blocks = decode_ssv_memory(bytes, bytes.len()).unwrap();
let carte_ps = decode_carte_ps(blocks).unwrap();
assert_eq!(carte_ps.titulaire.type_de_carte_ps, "0");
@ -370,7 +362,7 @@ mod test_decode_carte_ps {
57, 53, 8, 48, 48, 50, 48, 50, 52, 49, 57, 1, 56, 0, 1, 48, 1, 49, 2, 53, 48, 2, 49,
48, 2, 48, 48, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49,
];
let blocks = decode_ssv_memory(bytes, bytes.len());
let blocks = decode_ssv_memory(bytes, bytes.len()).unwrap();
let carte_ps = decode_carte_ps(blocks).unwrap();
assert_eq!(carte_ps.situations.len(), 3);

View File

@ -1,5 +1,4 @@
pub mod cps;
pub mod cartevitale;
pub mod libssv;
pub mod ssv_memory;
pub mod ssvlib_demo;

View File

@ -3,6 +3,13 @@
/// Low level bindings to the SSVLIB dynamic library.
// TODO : look for creating a dedicated *-sys crate : https://kornel.ski/rust-sys-crate
use libc::{c_char, c_ushort, c_void, size_t};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum LibSSVError {
#[error("SSV library error in {function}: {code}")]
StandardErrorCode { code: u16, function: &'static str },
}
#[cfg_attr(target_os = "linux", link(name = "ssvlux64"))]
#[cfg_attr(target_os = "windows", link(name = "ssvw64"))]
@ -19,13 +26,5 @@ extern "C" {
ZDonneesSortie: *mut *mut c_void,
TTailleDonneesSortie: *mut size_t,
) -> c_ushort;
pub fn SSV_LireDroitsVitale(
NomRessourcePS: *const c_char,
NomRessourceLecteur: *const c_char,
CodePorteurPS: *const c_char,
DateConsultation: *const c_char,
ZDonneesSortie: *mut *mut c_void,
TTailleDonneesSortie: *mut size_t,
) -> c_ushort;
}
// TODO : replace void* by Rust struct : https://doc.rust-lang.org/nomicon/ffi.html#representing-opaque-structs

View File

@ -1,9 +1,10 @@
mod cps;
mod cartevitale;
mod libssv;
mod ssv_memory;
mod ssvlib_demo;
fn main() {
ssvlib_demo::demo();
use anyhow::{Context, Result};
fn main() -> Result<()> {
ssvlib_demo::demo().context("Error while running the SSV library demo")
}

View File

@ -1,6 +1,33 @@
/// # SSV Memory
/// Provide functions to manipulate raw memory from SSV library.
use std::convert::TryFrom;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum BytesReadingError {
#[error("Empty bytes input")]
EmptyBytes,
#[error("Invalid memory: not enough bytes ({actual}) to read the expected size ({expected})")]
InvalidSize { expected: usize, actual: usize },
#[error("Invalid memory: size ({actual}) is expected to be less than {expected} bytes")]
SizeTooBig { expected: usize, actual: usize },
#[error("Invalid memory: not enough bytes to read the block id")]
InvalidBlockId(#[from] std::array::TryFromSliceError),
#[error("Error while reading field at offset {offset}")]
InvalidField {
source: Box<BytesReadingError>,
offset: usize,
},
}
#[derive(Debug, Error)]
pub enum SSVMemoryError {
#[error("Error while parsing block at offset {offset}")]
BlockParsing {
source: BytesReadingError,
offset: usize,
},
}
#[derive(PartialEq, Debug)]
struct ElementSize {
@ -9,13 +36,12 @@ struct ElementSize {
}
// TODO : Est-ce qu'on pourrait/devrait définir un type custom pour représenter les tableaux de bytes ?
impl TryFrom<&[u8]> for ElementSize {
type Error = &'static str;
type Error = BytesReadingError;
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
if bytes.is_empty() {
return Err("Empty bytes input");
return Err(BytesReadingError::EmptyBytes);
}
let mut element_size = ElementSize { size: 0, pad: 1 };
@ -30,9 +56,15 @@ impl TryFrom<&[u8]> for ElementSize {
// N are the 7 lower bits of the first byte
let size_bytes_len = (bytes[0] & 0b0111_1111) as usize;
if size_bytes_len > bytes.len() - 1 {
return Err("Invalid memory: not enough bytes to read the size");
return Err(BytesReadingError::InvalidSize {
expected: size_bytes_len,
actual: bytes.len() - 1,
});
} else if size_bytes_len > 4 {
return Err("Invalid memory: size is too big");
return Err(BytesReadingError::SizeTooBig {
expected: 4,
actual: size_bytes_len,
});
}
let size_bytes = &bytes[1..1 + size_bytes_len];
@ -54,15 +86,21 @@ pub struct Block<'a> {
pub content: Vec<Field<'a>>,
}
impl<'a> From<&'a [u8]> for Block<'a> {
fn from(bytes: &'a [u8]) -> Self {
impl<'a> TryFrom<&'a [u8]> for Block<'a> {
type Error = BytesReadingError;
fn try_from(bytes: &'a [u8]) -> Result<Self, Self::Error> {
let mut offset = 0;
let id = u16::from_be_bytes(bytes[..2].try_into().unwrap());
let id = u16::from_be_bytes(
bytes[..2]
.try_into()
.map_err(BytesReadingError::InvalidBlockId)?,
);
offset += 2;
let ElementSize {
size: block_size,
pad,
} = bytes[2..].try_into().unwrap();
} = bytes[2..].try_into()?;
offset += pad;
let raw_content = &bytes[offset..];
let mut field_offset = 0;
@ -70,19 +108,22 @@ impl<'a> From<&'a [u8]> for Block<'a> {
let mut content = Vec::new();
let mut field_id = 1;
while field_offset < block_size {
let mut field: Field<'a> = raw_content[field_offset..].into();
let mut field: Field<'a> = raw_content[field_offset..].try_into().map_err(|err| {
BytesReadingError::InvalidField {
source: Box::new(err),
offset: field_offset,
}
})?;
field.id = field_id;
field_offset += field.size;
field_id += 1;
content.push(field);
}
Block {
Ok(Block {
id,
size: offset + block_size,
content,
}
})
}
}
@ -93,31 +134,41 @@ pub struct Field<'a> {
pub content: &'a [u8],
}
impl<'a> From<&'a [u8]> for Field<'a> {
fn from(bytes: &'a [u8]) -> Self {
let ElementSize { size, pad } = bytes.try_into().unwrap();
impl<'a> TryFrom<&'a [u8]> for Field<'a> {
type Error = BytesReadingError;
fn try_from(bytes: &'a [u8]) -> Result<Self, Self::Error> {
let ElementSize { size, pad } = bytes.try_into()?;
let contenu = &bytes[pad..pad + size];
Field {
Ok(Field {
id: 0,
size: pad + size,
content: contenu,
}
})
}
}
pub fn decode_ssv_memory(bytes: &[u8], size: usize) -> Vec<Block> {
pub fn decode_ssv_memory(bytes: &[u8], size: usize) -> Result<Vec<Block>, SSVMemoryError> {
let mut blocks: Vec<Block> = Vec::new();
let mut offset = 0;
while offset < size {
let block: Block = bytes[offset..].into();
let block: Block =
bytes[offset..]
.try_into()
.map_err(|err| SSVMemoryError::BlockParsing {
source: err,
offset,
})?;
offset += block.size;
blocks.push(block);
}
blocks
Ok(blocks)
}
#[cfg(test)]
mod test_element_size {
use std::any::Any;
use super::*;
#[test]
@ -144,29 +195,51 @@ mod test_element_size {
#[test]
fn null_size() {
let bytes: &[u8] = &[];
let result: Result<ElementSize, &str> = bytes.try_into();
assert_eq!(result, Err("Empty bytes input"),);
let result: Result<ElementSize, BytesReadingError> = bytes.try_into();
assert!(result.is_err());
assert_eq!(
result.unwrap_err().type_id(),
BytesReadingError::EmptyBytes.type_id()
);
}
#[test]
fn invalid_memory() {
let bytes: &[u8] = &[0b_1000_0001_u8];
let result: Result<ElementSize, &str> = bytes.try_into();
let result: Result<ElementSize, BytesReadingError> = bytes.try_into();
assert!(result.is_err());
assert_eq!(
result,
Err("Invalid memory: not enough bytes to read the size"),
result.unwrap_err().to_string(),
BytesReadingError::InvalidSize {
expected: 1,
actual: 0
}
.to_string()
);
let bytes: &[u8] = &[0b_1000_0010_u8, 1];
let result: Result<ElementSize, &str> = bytes.try_into();
let result: Result<ElementSize, BytesReadingError> = bytes.try_into();
assert!(result.is_err());
assert_eq!(
result,
Err("Invalid memory: not enough bytes to read the size"),
result.unwrap_err().to_string(),
BytesReadingError::InvalidSize {
expected: 2,
actual: 1
}
.to_string()
);
let bytes: &[u8] = &[0b_1000_0101_u8, 1, 1, 1, 1, 1];
let result: Result<ElementSize, &str> = bytes.try_into();
assert_eq!(result, Err("Invalid memory: size is too big"),);
let result: Result<ElementSize, BytesReadingError> = bytes.try_into();
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
BytesReadingError::SizeTooBig {
expected: 4,
actual: 5
}
.to_string()
);
}
}
@ -181,7 +254,7 @@ mod test_field {
80, 72, 65, 82, 77, 65, 67, 73, 69, 78, 48, 48, 53, 50, 52, 49, 57, 9, 70, 82, 65, 78,
67, 79, 73, 83, 69, 1, 84,
];
let element: Field = bytes.into();
let element: Field = bytes.try_into().unwrap();
assert_eq!(element.size, 52);
assert_eq!(element.content[..5], [1, 48, 1, 56, 11]);
}
@ -196,7 +269,7 @@ mod test_field {
// Add 256 bytes to the content
bytes_vec.append(&mut vec![1; 256]);
let bytes: &[u8] = &bytes_vec;
let element: Field = bytes.into();
let element: Field = bytes.try_into().unwrap();
assert_eq!(element.size, 259);
assert_eq!(element.content.len(), 256);
}
@ -210,15 +283,15 @@ mod test_block {
fn test_francoise_pharmacien0052419_partial_block_1() {
let bytes: &[u8] = &[1, 48, 1, 56, 11, 57, 57, 55, 48, 48, 53, 50, 52, 49, 57, 52];
let field1: Field = bytes.into();
let field1: Field = bytes.try_into().unwrap();
assert_eq!(field1.size, 2);
assert_eq!(field1.content, &[48]);
let field2: Field = bytes[field1.size..].into();
let field2: Field = bytes[field1.size..].try_into().unwrap();
assert_eq!(field2.size, 2);
assert_eq!(field2.content, &[56]);
let field3: Field = bytes[field1.size + field2.size..].into();
let field3: Field = bytes[field1.size + field2.size..].try_into().unwrap();
assert_eq!(field3.size, 12);
assert_eq!(
field3.content,
@ -245,12 +318,12 @@ mod test_block {
48, 2, 49, 48, 2, 48, 48, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49,
];
let first_block: Block = bytes.into();
let first_block: Block = bytes.try_into().unwrap();
assert_eq!(first_block.id, 1);
assert_eq!(first_block.size, 54);
assert_eq!(first_block.content.len(), 8);
let second_block: Block = bytes[first_block.size..].into();
let second_block: Block = bytes[first_block.size..].try_into().unwrap();
assert_eq!(second_block.id, 2);
assert_eq!(second_block.size, 86);
assert_eq!(second_block.content.len(), 21);
@ -279,7 +352,7 @@ mod test_decode_ssv_memory {
50, 50, 49, 57, 53, 8, 48, 48, 50, 48, 50, 52, 49, 57, 1, 56, 0, 1, 48, 1, 49, 2, 53,
48, 2, 49, 48, 2, 48, 48, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49,
];
let blocks = decode_ssv_memory(bytes, bytes.len());
let blocks = decode_ssv_memory(bytes, bytes.len()).unwrap();
assert_eq!(blocks.len(), 2);
}
}

View File

@ -1,33 +1,54 @@
/// High level API for the SSV library,
/// based on the low level bindings in libssv.rs.
extern crate dotenv;
use libc::{c_void, size_t};
use std::env;
use std::ffi::CString;
use std::path::PathBuf;
use std::ptr;
use thiserror::Error;
use crate::cps::lire_carte;
use crate::cartevitale::LireDroitsVitale;
use crate::libssv::{SSV_InitLIB2, SSV_LireConfig};
fn ssv_init_lib_2() {
use ::utils::config::load_config;
#[derive(Error, Debug)]
pub enum SSVDemoError {
#[error(transparent)]
CartePSReading(#[from] crate::cps::CartePSError),
#[error(transparent)]
SSVLibErrorCode(#[from] crate::libssv::LibSSVError),
}
fn ssv_init_lib_2() -> Result<(), SSVDemoError> {
let ini_str = env::var("SESAM_INI_PATH").expect("SESAM_INI_PATH must be set");
let ini = CString::new(ini_str).expect("CString::new failed");
unsafe {
let result = SSV_InitLIB2(ini.as_ptr());
println!("SSV_InitLIB2 result: {}", result);
if result != 0 {
return Err(crate::libssv::LibSSVError::StandardErrorCode {
code: result,
function: "SSV_InitLIB2",
}
.into());
}
}
Ok(())
}
fn ssv_lire_config() {
fn ssv_lire_config() -> Result<(), SSVDemoError> {
let mut buffer: *mut c_void = ptr::null_mut();
let mut size: size_t = 0;
unsafe {
let result = SSV_LireConfig(&mut buffer, &mut size);
println!("SSV_LireConfig result: {}", result);
if result != 0 {
return Err(crate::libssv::LibSSVError::StandardErrorCode {
code: result,
function: "SSV_LireConfig",
}
.into());
}
if !buffer.is_null() {
let hex_values = std::slice::from_raw_parts(buffer as *const u8, size);
@ -39,30 +60,26 @@ fn ssv_lire_config() {
libc::free(buffer);
}
}
Ok(())
}
pub fn demo() {
pub fn demo() -> Result<(), SSVDemoError> {
// TODO : this is probably not working on release, because I'm not sure it exists a CARGO_MANIFEST_DIR and so it can find the `.env`
// Maybe we could use a system standard config path to store a config file
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let manifest_path = PathBuf::from(manifest_dir);
dotenv::from_path(manifest_path.join(".env")).ok();
println!("------- Demo for the SSV library --------");
ssv_init_lib_2();
load_config()?;
ssv_init_lib_2()?;
let code_pin = "1234";
let lecteur = "HID Global OMNIKEY 3x21 Smart Card Reader 0";
let lecteurvitale = "HID Global OMNIKEY 3x21 Smart Card Reader 1";
let dateconsultation ="20240813";
let carte_ps = lire_carte(code_pin, lecteur).unwrap();
let carte_ps = lire_carte(code_pin, lecteur)?;
println!("CartePS: {:#?}", carte_ps);
ssv_lire_config();
let carte_vitale = LireDroitsVitale(lecteur, lecteurvitale, code_pin, dateconsultation).unwrap();
println!("carte_vitale: {:#?}", carte_vitale);
ssv_lire_config()?;
println!("-----------------------------------------");
Ok(())
}

View File

@ -3,4 +3,3 @@ EXPORTS
SSV_InitLIB2
SSV_LireCartePS
SSV_LireConfig
SSV_LireDroitsVitale

9
crates/utils/Cargo.toml Normal file
View File

@ -0,0 +1,9 @@
[package]
name = "utils"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0"
directories = "5.0"
dotenv = "0.15"

View File

@ -0,0 +1,48 @@
use std::{env, path::PathBuf};
use anyhow::{bail, Context, Result};
use directories::ProjectDirs;
use dotenv::from_path;
const CONFIG_FILE_NAME: &str = ".env";
pub fn get_config_dirs() -> Vec<PathBuf> {
let mut config_dirs = vec![
PathBuf::from(""), // Current directory
];
if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") {
config_dirs.push(PathBuf::from(manifest_dir));
}
if let Some(proj_dirs) = ProjectDirs::from("org", "P4pillon", "Krys4lide") {
config_dirs.push(proj_dirs.config_dir().to_path_buf());
}
config_dirs
}
pub fn get_config_files() -> Result<Vec<PathBuf>> {
let config_dirs = get_config_dirs();
let mut config_files = Vec::new();
for config_dir in config_dirs.iter() {
let config_file = config_dir.join(CONFIG_FILE_NAME);
if config_file.exists() {
config_files.push(config_file);
}
}
if config_files.is_empty() {
bail!(
"No config file {CONFIG_FILE_NAME} found in the following directories: {config_dirs:#?}"
);
}
Ok(config_files)
}
pub fn load_config() -> Result<()> {
let config_files = get_config_files()?;
// Load the first config file found
// TODO: add a verbose log to list all config files found
println!(
"DEBUG: Config files found (1st loaded): {:#?}",
config_files
);
from_path(config_files[0].as_path()).context("Failed to load config file")
}

1
crates/utils/src/lib.rs Normal file
View File

@ -0,0 +1 @@
pub mod config;

85
docs/errors.md Normal file
View File

@ -0,0 +1,85 @@
# Gestion des erreurs
Ce document décrit comment les erreurs sont gérées dans le projet.
## Gestion native
Par principe, en Rust, on évite au maximum la gestion par exception, ne la réservant qu'aux situations où un crash du programme est la meilleure solution.
En temps normal, on renvoie des `Result<Valeur, Erreur>` (pour les situations réussite/erreur) ou des `Option<Valeur>` (pour les situations valeur non-nulle/nulle).
Quand on fait face à une situation d'erreur, on cherchera à la gérer de manière explicite (voir [Récupération des erreurs](#récupération-des-erreurs)) ou à la remonter à un niveau supérieur, généralement à l'aide de l'opérateur `?`.
On évitera, par contre, au maximum de générer des exceptions (appelées "panics" en Rust), que ce soit par l'usage de `panic!` ou par des appels à des fonctions qui paniquent en cas d'erreur (comme `unwrap` ou `expect`).
De nombreux exemples des idiomes natifs de gestion des erreurs en Rust sont disponibles dans la documentation [Rust by example](https://doc.rust-lang.org/rust-by-example/error.html).
## Librairies de gestion des erreurs
Deux librairies sont utilisées pour gérer les erreurs dans le projet :
- [`anyhow`](https://docs.rs/anyhow/latest/anyhow/) : qui permet de renvoyer des erreurs faiblement typées, mais très facile à enrichir avec des messages d'explication. On l'utilise pour communiquer facilement des erreurs de haut niveau avec un utilisateur final.
```rust
use anyhow::{anyhow, Result};
fn get_cluster_info() -> Result<ClusterInfo> {
let data = fs::read_to_string("cluster.json")
.with_context(|| "failed to read cluster config")?;
let info: ClusterInfo = serde_json::from_str(&data)
.with_context(|| "failed to parse cluster config")?;
Ok(info)
}
```
- [`thiserror`](https://docs.rs/thiserror/latest/thiserror/) : qui fournit des macros pour définir des erreurs fortement typées. On l'utilise pour définir des erreurs spécifiques à une partie du code, contextualisées avec des données structurées plutôt que de simples messages d'erreurs, afin de favoriser la "récupération" face aux erreurs.
```rust
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DataStoreError {
#[error("data store disconnected")]
Disconnect(#[from] io::Error),
#[error("the data for key `{0}` is not available")]
Redaction(String),
#[error("invalid header (expected {expected:?}, found {found:?})")]
InvalidHeader {
expected: String,
found: String,
},
#[error("unknown data store error")]
Unknown,
}
```
## Récupération des erreurs
Dans la mesure du possible, on essaie de privilégier la "récupération" face à une erreur plutôt que le "crash". Les stratégies de récupération sont :
- Réessayer l'opération, tel quel ou avec des paramètres différents
- Contourner l'opération, en la remplaçant par une autre
- À défaut, informer l'utilisateur de l'erreur et :
- arrêter / annuler l'opération en cours
- ignorer l'erreur et continuer l'exécution
Quand on ne peut pas récupérer une erreur, on la remonte à un niveau supérieur, si besoin en la convertissant dans un type d'erreur plus générique et approprié au niveau considéré.
## Conversion des erreurs
Quand on remonte une erreur à un niveau supérieur, on peut être amené à la convertir dans un type d'erreur plus générique et approprié au niveau considéré. Pour faciliter cette conversion, on implémente le trait `From`. Avec `thiserror`, on peut utiliser l'attribut `#[from]` ou le paramètre `source` pour automatiser l'implémentation de telles conversions.
On peut ensuite, lors de la gestion d'une erreur, on pourra :
- soit directement renvoyer l'erreur à l'aide de l'opérateur `?`, qui se chargera de la conversion ;
- soit convertir l'erreur explicitement, par exemple en utilisant la méthode `map_err` sur un `Result`, en particulier quand on veut enrichir l'erreur avec des informations supplémentaires.
## Usages exceptionnels de `unwrap` et `expect`
Provoquant des "panics" en cas d'erreur, les fonctions `unwrap` et `expect` ne doivent être utilisées que dans des cas exceptionnels :
- Dans les tests, pour signaler une erreur de test
- Au plus haut niveau de l'application, pour signaler une erreur fatale qui ne peut pas être récupérée
- Dans des situations où l'erreur ne peut pas se produire, par exemple après une vérification de préconditions
Dans l'idéal, on préférera l'usage de `expect` à `unwrap`, car il permet de donner un message d'erreur explicite.
## Ressources
- [The Rust Programming Language - Ch. 9: Error Handling](https://doc.rust-lang.org/book/ch09-00-error-handling.html)
- [The NRC Book - Error Handling in Rust](https://nrc.github.io/error-docs/intro.html)
- [The Error Design Patterns Book - by Project Error Handling WG](https://github.com/rust-lang/project-error-handling/blob/master/error-design-patterns-book/src/SUMMARY.md)
- [The Rust Cookbook - Error Handling](https://rust-lang-nursery.github.io/rust-cookbook/error-handling.html)
- [Le ticket initial de l'intégration de la gestion des erreurs dans le projet](https://forge.p4pillon.org/P4Pillon/Krys4lide/issues/34)

View File

@ -1,26 +0,0 @@
@echo off
rem Set variables
set LIB_DIR=crates/sesam-vitale/lib
set DEF_DIR=crates/sesam-vitale/src/win/fsv
if "%1"=="/clean" (
goto clean
)
rem Set the environment for the x64 platform
call "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat" x64
rem Create a ssvw64.lib file from the ssvw64.def file
lib /def:ssvw64.def /out:%LIB_DIR%\ssvw64.lib /machine:x64
rem Build complete
pause
exit /b 0
:clean
del %LIB_DIR%\*.lib
del %LIB_DIR%\*.exp
rem Clean complete
pause
exit /b 0

View File

@ -1,7 +1,7 @@
@echo off
rem Set variables
set LIB_DIR=F:\PAPILLON\Krys4lideCopie\crates\sesam-vitale\lib
set DEF_DIR=F:\PAPILLON\Krys4lideCopie\crates\sesam-vitale\src\win\fsv
set LIB_DIR=crates/sesam-vitale/lib
set DEF_DIR=crates/sesam-vitale/src/win/fsv
if "%1"=="/clean" (
goto clean

View File

@ -1,6 +0,0 @@
LIBRARY "ssvw64"
EXPORTS
SSV_InitLIB2
SSV_LireCartePS
SSV_LireConfig
SSV_LireDroitsVitale