Compare commits
	
		
			32 Commits
		
	
	
		
			338c6737e9
			...
			952c8561ba
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 952c8561ba | |||
| 9829a189a4 | |||
| c8f14cd77b | |||
| 6d9cd7fc14 | |||
| c0bbdcf030 | |||
| 488f719919 | |||
| 9b279ce4cd | |||
| 334e6520d5 | |||
| 68376383fa | |||
| 581bd2455e | |||
| 37ef6e2c15 | |||
| 1d5553a739 | |||
| eca3ea2cd6 | |||
| 241629c7ab | |||
| b9ac1a3587 | |||
| 6209c087c2 | |||
| cba1534fd7 | |||
|   | e03abbb87e | ||
| e6db84e9ac | |||
| 13254228a6 | |||
| f083c696f8 | |||
| 8b4fe4fd10 | |||
| 160dcc9249 | |||
|   | 016ae43402 | ||
| 01d17207fa | |||
| c82d3abd9c | |||
| cde7f3aab4 | |||
|   | 3a263f92e1 | ||
| 13b92aee9c | |||
| 3999532714 | |||
| e51dcc0ed0 | |||
| 0f7a291b3c | 
| @@ -24,10 +24,6 @@ body: | |||||||
|       label: Ce problème est il relatif à un ou des modules en particulier ? |       label: Ce problème est il relatif à un ou des modules en particulier ? | ||||||
|       multiple: true |       multiple: true | ||||||
|       options: |       options: | ||||||
|         - Interface utilisateur⋅ice (crates/app) |         - Clego | ||||||
|         - Encapsulation Tauri (crates/desktop) |         - Tauri | ||||||
|         - Moteur SESAM-Vitale (crates/sesam-vitale) |         - Axum | ||||||
|         - Librairie utilitaire (crates/utils) |  | ||||||
|         - Documentation (docs) |  | ||||||
|         - Scripts (scripts) |  | ||||||
|         - Autre |  | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -21,5 +21,3 @@ target/ | |||||||
| *.sln | *.sln | ||||||
| *.sw? | *.sw? | ||||||
|  |  | ||||||
| # Ignore .env files |  | ||||||
| .env |  | ||||||
|   | |||||||
							
								
								
									
										5
									
								
								.ignore
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								.ignore
									
									
									
									
									
								
							| @@ -1,5 +0,0 @@ | |||||||
| # Ignorer les fichiers dont ne dépent pas la compilation  |  | ||||||
| *.md |  | ||||||
| tailwind.config.js |  | ||||||
| *.example |  | ||||||
| scripts |  | ||||||
							
								
								
									
										1641
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1641
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -5,5 +5,4 @@ members = [ | |||||||
|     "crates/sesam-vitale", |     "crates/sesam-vitale", | ||||||
|     "crates/desktop", |     "crates/desktop", | ||||||
|     "crates/services-sesam-vitale-sys", |     "crates/services-sesam-vitale-sys", | ||||||
|     "crates/utils", |  | ||||||
| ] | ] | ||||||
|   | |||||||
							
								
								
									
										27
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								README.md
									
									
									
									
									
								
							| @@ -7,20 +7,6 @@ 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` | - `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` | - `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 ...) | - `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 | ## Development | ||||||
|  |  | ||||||
| @@ -61,19 +47,6 @@ Si vous souhaitez lancer les composants séparément, les indications de lanceme | |||||||
| - [app](crates/app/README.md) | - [app](crates/app/README.md) | ||||||
| - [sesam-vitale](crates/sesam-vitale/README.md) | - [sesam-vitale](crates/sesam-vitale/README.md) | ||||||
|  |  | ||||||
| ## Rechargement automatique |  | ||||||
|  |  | ||||||
| Pour permettre de développer plus rapidement, il existe une librairie qui recompile automatiquement nos modifications en cours : [`cargo-watch`](https://github.com/watchexec/cargo-watch) permet de relancer une commande `cargo` lorsqu'un fichier est modifié (example: `cargo run` --> `cargo watch -x run`). |  | ||||||
|  |  | ||||||
| Voici la commande pour l'installer dans un _package_ : |  | ||||||
| ```bash |  | ||||||
| cargo add cargo-watch --dev --package app |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| Le fichier [`.ignore`](./ignore) permet d'ignorer certains fichiers pour éviter de relancer la recompilation inutilement. |  | ||||||
|  |  | ||||||
| ⚠️ La librairie n'est pas compatible avec _Windows 7_ et les versions antérieurs de _Windows_. |  | ||||||
|  |  | ||||||
| ## Build | ## Build | ||||||
|  |  | ||||||
| Packager le client desktop | Packager le client desktop | ||||||
|   | |||||||
| @@ -7,15 +7,7 @@ edition = "2021" | |||||||
| askama = "0.12.1" | askama = "0.12.1" | ||||||
| askama_axum = "0.4.0" | askama_axum = "0.4.0" | ||||||
| axum = "0.7.5" | 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"] } | serde = { version = "1.0.204", features = ["derive"] } | ||||||
| thiserror = "1.0.63" |  | ||||||
| tokio = { version = "1.39.1", features = ["macros", "rt-multi-thread"] } | tokio = { version = "1.39.1", features = ["macros", "rt-multi-thread"] } | ||||||
| tower-http = { version = "0.5.2", features = ["fs"] } | tower-http = { version = "0.5.2", features = ["fs"] } | ||||||
| tower-livereload = "0.9.3" |  | ||||||
|  |  | ||||||
| [dev-dependencies] |  | ||||||
| cargo-watch = "8.5.1" |  | ||||||
| systemfd = "0.4.0" |  | ||||||
|   | |||||||
| @@ -13,23 +13,3 @@ | |||||||
| ```bash | ```bash | ||||||
| cargo run --bin app | cargo run --bin app | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ## Rechargement automatique (_auto-reload_) |  | ||||||
|  |  | ||||||
| Pour le projet `app`, nous utilisons en plus de `cargo-watch` ses librairies : |  | ||||||
| - [`systemfd`](https://github.com/mitsuhiko/systemfd) permet de redémarrer un serveur sans interrompre les connexions en cours, il transmet le descripteur de fichier du socket à une nouvelle instance du serveur (exemple: `cargo watch -x run` --> `systemfd --no-pid -s http::3000 -- cargo watch -x run`). Si le port est déjà pris il en prendra un autre. |  | ||||||
| - [`listenfd`](https://github.com/mitsuhiko/listenfd) permet, côté _Rust_, de démarrer un serveur en utilisant des connexions déjà ouvertes. |  | ||||||
|  |  | ||||||
| Pour notre application voici la commande à lancer : |  | ||||||
|  |  | ||||||
| ```bash |  | ||||||
| systemfd --no-pid -s http::3000 -- cargo watch -x 'run --bin app' |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ## Chargement à chaud (_livereload_) |  | ||||||
|  |  | ||||||
| Pour que notre navigateur rafraîchisse automatique notre page lorsque le serveur a été recompilé, nous utilisons la librairie [`tower-livereload`](https://github.com/leotaku/tower-livereload). |  | ||||||
|  |  | ||||||
| A chaque changement, que ça soit sur du code en _Rust_, _HTML_, _CSS_ ou _JS_ alors le navigateur va recharger entièrement la page. |  | ||||||
|  |  | ||||||
| En Rust, il n'existe pas encore d'outil de _Hot Reload_ complet et intégré comme on en trouve dans d'autres environnements de développement web, comme pour _Node.js_. |  | ||||||
|   | |||||||
| @@ -1,6 +0,0 @@ | |||||||
| [general] |  | ||||||
| # Directories to search for templates, relative to the crate root. |  | ||||||
| dirs = [ |  | ||||||
|     "src/pages", |  | ||||||
|     "src/components", |  | ||||||
| ] |  | ||||||
							
								
								
									
										1
									
								
								crates/app/assets/css/flowbite@2.5.1.min.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								crates/app/assets/css/flowbite@2.5.1.min.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -566,8 +566,28 @@ video { | |||||||
|   border-width: 0; |   border-width: 0; | ||||||
| } | } | ||||||
|  |  | ||||||
| .z-50 { | .absolute { | ||||||
|   z-index: 50; |   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; | ||||||
| } | } | ||||||
|  |  | ||||||
| .mx-auto { | .mx-auto { | ||||||
| @@ -575,11 +595,38 @@ video { | |||||||
|   margin-right: auto; |   margin-right: auto; | ||||||
| } | } | ||||||
|  |  | ||||||
| .my-4 { | .-mr-2 { | ||||||
|   margin-top: 1rem; |   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; |   margin-bottom: 1rem; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .mt-4 { | ||||||
|  |   margin-top: 1rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .mt-6 { | ||||||
|  |   margin-top: 1.5rem; | ||||||
|  | } | ||||||
|  |  | ||||||
| .mb-2 { | .mb-2 { | ||||||
|   margin-bottom: 0.5rem; |   margin-bottom: 0.5rem; | ||||||
| } | } | ||||||
| @@ -588,22 +635,10 @@ video { | |||||||
|   margin-bottom: 0.625rem; |   margin-bottom: 0.625rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| .mb-4 { |  | ||||||
|   margin-bottom: 1rem; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .me-3 { | .me-3 { | ||||||
|   margin-inline-end: 0.75rem; |   margin-inline-end: 0.75rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| .mt-3 { |  | ||||||
|   margin-top: 0.75rem; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .mt-4 { |  | ||||||
|   margin-top: 1rem; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .block { | .block { | ||||||
|   display: block; |   display: block; | ||||||
| } | } | ||||||
| @@ -628,44 +663,52 @@ video { | |||||||
|   height: 2.5rem; |   height: 2.5rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| .h-2 { | .h-16 { | ||||||
|   height: 0.5rem; |   height: 4rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| .h-2\.5 { | .h-6 { | ||||||
|   height: 0.625rem; |   height: 1.5rem; | ||||||
| } |  | ||||||
|  |  | ||||||
| .h-32 { |  | ||||||
|   height: 8rem; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .h-4 { |  | ||||||
|   height: 1rem; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .h-48 { |  | ||||||
|   height: 12rem; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .h-5 { |  | ||||||
|   height: 1.25rem; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .h-7 { |  | ||||||
|   height: 1.75rem; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| .h-8 { | .h-8 { | ||||||
|   height: 2rem; |   height: 2rem; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .h-full { | ||||||
|  |   height: 100%; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .h-32 { | ||||||
|  |   height: 8rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .h-48 { | ||||||
|  |   height: 12rem; | ||||||
|  | } | ||||||
|  |  | ||||||
| .h-96 { | .h-96 { | ||||||
|   height: 24rem; |   height: 24rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| .h-full { | .h-2\.5 { | ||||||
|   height: 100%; |   height: 0.625rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .h-9 { | ||||||
|  |   height: 2.25rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .h-7 { | ||||||
|  |   height: 1.75rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .h-2 { | ||||||
|  |   height: 0.5rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .h-4 { | ||||||
|  |   height: 1rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| .min-h-full { | .min-h-full { | ||||||
| @@ -676,38 +719,58 @@ video { | |||||||
|   width: 2.5rem; |   width: 2.5rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| .w-32 { |  | ||||||
|   width: 8rem; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .w-48 { | .w-48 { | ||||||
|   width: 12rem; |   width: 12rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| .w-5 { | .w-6 { | ||||||
|   width: 1.25rem; |   width: 1.5rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| .w-8 { | .w-8 { | ||||||
|   width: 2rem; |   width: 2rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| .w-full { | .w-auto { | ||||||
|   width: 100%; |   width: auto; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .w-32 { | ||||||
|  |   width: 8rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .w-20 { | ||||||
|  |   width: 5rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .w-24 { | ||||||
|  |   width: 6rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .w-28 { | ||||||
|  |   width: 7rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| .max-w-7xl { | .max-w-7xl { | ||||||
|   max-width: 80rem; |   max-width: 80rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| .max-w-screen-xl { | .max-w-xs { | ||||||
|   max-width: 1280px; |   max-width: 20rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| .max-w-sm { | .max-w-sm { | ||||||
|   max-width: 24rem; |   max-width: 24rem; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .flex-shrink-0 { | ||||||
|  |   flex-shrink: 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .origin-top-right { | ||||||
|  |   transform-origin: top right; | ||||||
|  | } | ||||||
|  |  | ||||||
| @keyframes pulse { | @keyframes pulse { | ||||||
|   50% { |   50% { | ||||||
|     opacity: .5; |     opacity: .5; | ||||||
| @@ -718,20 +781,12 @@ video { | |||||||
|   animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; |   animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; | ||||||
| } | } | ||||||
|  |  | ||||||
| .list-none { |  | ||||||
|   list-style-type: none; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .grid-cols-1 { | .grid-cols-1 { | ||||||
|   grid-template-columns: repeat(1, minmax(0, 1fr)); |   grid-template-columns: repeat(1, minmax(0, 1fr)); | ||||||
| } | } | ||||||
|  |  | ||||||
| .flex-col { | .grid-cols-2 { | ||||||
|   flex-direction: column; |   grid-template-columns: repeat(2, minmax(0, 1fr)); | ||||||
| } |  | ||||||
|  |  | ||||||
| .flex-wrap { |  | ||||||
|   flex-wrap: wrap; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| .items-center { | .items-center { | ||||||
| @@ -750,64 +805,46 @@ video { | |||||||
|   gap: 1rem; |   gap: 1rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| .space-x-3 > :not([hidden]) ~ :not([hidden]) { | .space-y-1 > :not([hidden]) ~ :not([hidden]) { | ||||||
|   --tw-space-x-reverse: 0; |   --tw-space-y-reverse: 0; | ||||||
|   margin-right: calc(0.75rem * var(--tw-space-x-reverse)); |   margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse))); | ||||||
|   margin-left: calc(0.75rem * calc(1 - var(--tw-space-x-reverse))); |   margin-bottom: calc(0.25rem * var(--tw-space-y-reverse)); | ||||||
| } |  | ||||||
|  |  | ||||||
| .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)); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .divide-gray-100 > :not([hidden]) ~ :not([hidden]) { |  | ||||||
|   --tw-divide-opacity: 1; |  | ||||||
|   border-color: rgb(243 244 246 / var(--tw-divide-opacity)); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .self-center { |  | ||||||
|   align-self: center; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .truncate { |  | ||||||
|   overflow: hidden; |  | ||||||
|   text-overflow: ellipsis; |  | ||||||
|   white-space: nowrap; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .whitespace-nowrap { |  | ||||||
|   white-space: nowrap; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .rounded { |  | ||||||
|   border-radius: 0.25rem; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| .rounded-full { | .rounded-full { | ||||||
|   border-radius: 9999px; |   border-radius: 9999px; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .rounded-md { | ||||||
|  |   border-radius: 0.375rem; | ||||||
|  | } | ||||||
|  |  | ||||||
| .rounded-lg { | .rounded-lg { | ||||||
|   border-radius: 0.5rem; |   border-radius: 0.5rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| .border { | .rounded { | ||||||
|   border-width: 1px; |   border-radius: 0.25rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| .border-2 { | .border-2 { | ||||||
|   border-width: 2px; |   border-width: 2px; | ||||||
| } | } | ||||||
|  |  | ||||||
| .border-dashed { | .border { | ||||||
|   border-style: dashed; |   border-width: 1px; | ||||||
| } | } | ||||||
|  |  | ||||||
| .border-gray-100 { | .border-b { | ||||||
|   --tw-border-opacity: 1; |   border-bottom-width: 1px; | ||||||
|   border-color: rgb(243 244 246 / var(--tw-border-opacity)); | } | ||||||
|  |  | ||||||
|  | .border-t { | ||||||
|  |   border-top-width: 1px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .border-dashed { | ||||||
|  |   border-style: dashed; | ||||||
| } | } | ||||||
|  |  | ||||||
| .border-gray-200 { | .border-gray-200 { | ||||||
| @@ -820,9 +857,9 @@ video { | |||||||
|   border-color: rgb(209 213 219 / var(--tw-border-opacity)); |   border-color: rgb(209 213 219 / var(--tw-border-opacity)); | ||||||
| } | } | ||||||
|  |  | ||||||
| .bg-blue-700 { | .bg-white { | ||||||
|   --tw-bg-opacity: 1; |   --tw-bg-opacity: 1; | ||||||
|   background-color: rgb(29 78 216 / var(--tw-bg-opacity)); |   background-color: rgb(255 255 255 / var(--tw-bg-opacity)); | ||||||
| } | } | ||||||
|  |  | ||||||
| .bg-gray-200 { | .bg-gray-200 { | ||||||
| @@ -835,19 +872,8 @@ video { | |||||||
|   background-color: rgb(209 213 219 / var(--tw-bg-opacity)); |   background-color: rgb(209 213 219 / var(--tw-bg-opacity)); | ||||||
| } | } | ||||||
|  |  | ||||||
| .bg-gray-50 { | .p-1 { | ||||||
|   --tw-bg-opacity: 1; |   padding: 0.25rem; | ||||||
|   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 { | .p-2 { | ||||||
| @@ -858,39 +884,36 @@ video { | |||||||
|   padding: 1rem; |   padding: 1rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| .px-3 { |  | ||||||
|   padding-left: 0.75rem; |  | ||||||
|   padding-right: 0.75rem; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .px-4 { | .px-4 { | ||||||
|   padding-left: 1rem; |   padding-left: 1rem; | ||||||
|   padding-right: 1rem; |   padding-right: 1rem; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .py-1 { | ||||||
|  |   padding-top: 0.25rem; | ||||||
|  |   padding-bottom: 0.25rem; | ||||||
|  | } | ||||||
|  |  | ||||||
| .py-10 { | .py-10 { | ||||||
|   padding-top: 2.5rem; |   padding-top: 2.5rem; | ||||||
|   padding-bottom: 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 { | .py-8 { | ||||||
|   padding-top: 2rem; |   padding-top: 2rem; | ||||||
|   padding-bottom: 2rem; |   padding-bottom: 2rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| .text-2xl { | .pb-3 { | ||||||
|   font-size: 1.5rem; |   padding-bottom: 0.75rem; | ||||||
|   line-height: 2rem; | } | ||||||
|  |  | ||||||
|  | .pt-2 { | ||||||
|  |   padding-top: 0.5rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .pt-4 { | ||||||
|  |   padding-top: 1rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| .text-3xl { | .text-3xl { | ||||||
| @@ -916,10 +939,6 @@ video { | |||||||
|   font-weight: 500; |   font-weight: 500; | ||||||
| } | } | ||||||
|  |  | ||||||
| .font-semibold { |  | ||||||
|   font-weight: 600; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .leading-tight { | .leading-tight { | ||||||
|   line-height: 1.25; |   line-height: 1.25; | ||||||
| } | } | ||||||
| @@ -928,9 +947,9 @@ video { | |||||||
|   letter-spacing: -0.025em; |   letter-spacing: -0.025em; | ||||||
| } | } | ||||||
|  |  | ||||||
| .text-gray-200 { | .text-gray-400 { | ||||||
|   --tw-text-opacity: 1; |   --tw-text-opacity: 1; | ||||||
|   color: rgb(229 231 235 / var(--tw-text-opacity)); |   color: rgb(156 163 175 / var(--tw-text-opacity)); | ||||||
| } | } | ||||||
|  |  | ||||||
| .text-gray-500 { | .text-gray-500 { | ||||||
| @@ -938,9 +957,9 @@ video { | |||||||
|   color: rgb(107 114 128 / var(--tw-text-opacity)); |   color: rgb(107 114 128 / var(--tw-text-opacity)); | ||||||
| } | } | ||||||
|  |  | ||||||
| .text-gray-700 { | .text-gray-800 { | ||||||
|   --tw-text-opacity: 1; |   --tw-text-opacity: 1; | ||||||
|   color: rgb(55 65 81 / var(--tw-text-opacity)); |   color: rgb(31 41 55 / var(--tw-text-opacity)); | ||||||
| } | } | ||||||
|  |  | ||||||
| .text-gray-900 { | .text-gray-900 { | ||||||
| @@ -948,9 +967,19 @@ video { | |||||||
|   color: rgb(17 24 39 / var(--tw-text-opacity)); |   color: rgb(17 24 39 / var(--tw-text-opacity)); | ||||||
| } | } | ||||||
|  |  | ||||||
| .text-white { | .text-gray-200 { | ||||||
|   --tw-text-opacity: 1; |   --tw-text-opacity: 1; | ||||||
|   color: rgb(255 255 255 / var(--tw-text-opacity)); |   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); | ||||||
| } | } | ||||||
|  |  | ||||||
| .shadow { | .shadow { | ||||||
| @@ -959,11 +988,31 @@ video { | |||||||
|   box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); |   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 { | .hover\:bg-gray-100:hover { | ||||||
|   --tw-bg-opacity: 1; |   --tw-bg-opacity: 1; | ||||||
|   background-color: rgb(243 244 246 / var(--tw-bg-opacity)); |   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 { | .focus\:outline-none:focus { | ||||||
|   outline: 2px solid transparent; |   outline: 2px solid transparent; | ||||||
|   outline-offset: 2px; |   outline-offset: 2px; | ||||||
| @@ -975,27 +1024,47 @@ video { | |||||||
|   box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); |   box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); | ||||||
| } | } | ||||||
|  |  | ||||||
| .focus\:ring-4:focus { | .focus\:ring-indigo-500:focus { | ||||||
|   --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); |   --tw-ring-opacity: 1; | ||||||
|   --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color); |   --tw-ring-color: rgb(99 102 241 / var(--tw-ring-opacity)); | ||||||
|   box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| .focus\:ring-gray-200:focus { | .focus\:ring-offset-2:focus { | ||||||
|   --tw-ring-opacity: 1; |   --tw-ring-offset-width: 2px; | ||||||
|   --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) { | @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 { |   .sm\:grid-cols-2 { | ||||||
|     grid-template-columns: repeat(2, minmax(0, 1fr)); |     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 { |   .sm\:px-6 { | ||||||
|     padding-left: 1.5rem; |     padding-left: 1.5rem; | ||||||
|     padding-right: 1.5rem; |     padding-right: 1.5rem; | ||||||
| @@ -1003,91 +1072,28 @@ video { | |||||||
| } | } | ||||||
|  |  | ||||||
| @media (min-width: 768px) { | @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 { |   .md\:h-64 { | ||||||
|     height: 16rem; |     height: 16rem; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   .md\:w-auto { |   .md\:h-72 { | ||||||
|     width: auto; |     height: 18rem; | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .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 { |   .md\:p-6 { | ||||||
|     padding: 1.5rem; |     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) { | @media (min-width: 1024px) { | ||||||
|  |   .lg\:block { | ||||||
|  |     display: block; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .lg\:hidden { | ||||||
|  |     display: none; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   .lg\:grid-cols-4 { |   .lg\:grid-cols-4 { | ||||||
|     grid-template-columns: repeat(4, minmax(0, 1fr)); |     grid-template-columns: repeat(4, minmax(0, 1fr)); | ||||||
|   } |   } | ||||||
| @@ -1098,16 +1104,7 @@ video { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| .rtl\:space-x-reverse:where([dir="rtl"], [dir="rtl"] *) > :not([hidden]) ~ :not([hidden]) { |  | ||||||
|   --tw-space-x-reverse: 1; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @media (prefers-color-scheme: dark) { | @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 { |   .dark\:border-gray-600 { | ||||||
|     --tw-border-opacity: 1; |     --tw-border-opacity: 1; | ||||||
|     border-color: rgb(75 85 99 / var(--tw-border-opacity)); |     border-color: rgb(75 85 99 / var(--tw-border-opacity)); | ||||||
| @@ -1123,26 +1120,6 @@ video { | |||||||
|     background-color: rgb(55 65 81 / var(--tw-bg-opacity)); |     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 { |   .dark\:text-gray-600 { | ||||||
|     --tw-text-opacity: 1; |     --tw-text-opacity: 1; | ||||||
|     color: rgb(75 85 99 / var(--tw-text-opacity)); |     color: rgb(75 85 99 / var(--tw-text-opacity)); | ||||||
| @@ -1152,52 +1129,4 @@ video { | |||||||
|     --tw-text-opacity: 1; |     --tw-text-opacity: 1; | ||||||
|     color: rgb(55 65 81 / var(--tw-text-opacity)); |     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)); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,18 +0,0 @@ | |||||||
| {% 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> |  | ||||||
| @@ -1,50 +0,0 @@ | |||||||
| {% 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 %} |  | ||||||
| @@ -1,21 +1,30 @@ | |||||||
|  | mod pages; | ||||||
|  | mod templates; | ||||||
|  |  | ||||||
| use std::path::Path; | use std::path::Path; | ||||||
|  |  | ||||||
|  | use askama::Template; | ||||||
|  | use askama_axum::IntoResponse; | ||||||
| use axum::http::{StatusCode, Uri}; | use axum::http::{StatusCode, Uri}; | ||||||
| use axum_htmx::AutoVaryLayer; |  | ||||||
| use tower_http::services::ServeDir; | use tower_http::services::ServeDir; | ||||||
|  |  | ||||||
| mod menu; |  | ||||||
| mod pages; |  | ||||||
|  |  | ||||||
| async fn fallback(uri: Uri) -> (StatusCode, String) { | async fn fallback(uri: Uri) -> (StatusCode, String) { | ||||||
|     (StatusCode::NOT_FOUND, format!("No route for {uri}")) |     (StatusCode::NOT_FOUND, format!("No route for {uri}")) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #[derive(Template)] | ||||||
|  | #[template(path = "index.html")] | ||||||
|  | pub struct GetIndexResponse; | ||||||
|  |  | ||||||
|  | async fn root() -> impl IntoResponse { | ||||||
|  |     GetIndexResponse {}.into_response() | ||||||
|  | } | ||||||
|  |  | ||||||
| pub fn get_router(assets_path: &Path) -> axum::Router { | pub fn get_router(assets_path: &Path) -> axum::Router { | ||||||
|     axum::Router::new() |     axum::Router::new() | ||||||
|         .nest_service("/assets", ServeDir::new(assets_path)) |         .nest_service("/assets", ServeDir::new(assets_path)) | ||||||
|         .merge(pages::get_routes()) |         .route("/", axum::routing::get(root)) | ||||||
|  |         .nest("/pages", pages::get_routes()) | ||||||
|  |         .merge(templates::get_routes()) | ||||||
|         .fallback(fallback) |         .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) |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,84 +1,15 @@ | |||||||
| use std::path::{Path, PathBuf}; |  | ||||||
| use std::{env, io}; |  | ||||||
|  |  | ||||||
| use axum::body::Body; |  | ||||||
| use axum::http::Request; |  | ||||||
| use listenfd::ListenFd; |  | ||||||
| use notify::Watcher; |  | ||||||
| use thiserror::Error; |  | ||||||
| use tokio::net::TcpListener; |  | ||||||
| use tower_livereload::predicate::Predicate; |  | ||||||
| use tower_livereload::LiveReloadLayer; |  | ||||||
|  |  | ||||||
| use ::app::get_router; | use ::app::get_router; | ||||||
|  | use std::env; | ||||||
| #[derive(Error, Debug)] | use std::path::Path; | ||||||
| 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)] |  | ||||||
| struct NotHtmxPredicate; |  | ||||||
| impl<T> Predicate<Request<T>> for NotHtmxPredicate { |  | ||||||
|     fn check(&mut self, req: &Request<T>) -> bool { |  | ||||||
|         !(req.headers().contains_key("hx-request")) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const DEFAULT_LISTENER: &str = "localhost:3000"; |  | ||||||
| async fn get_tcp_listener() -> Result<TcpListener, io::Error> { |  | ||||||
|     let mut listenfd = ListenFd::from_env(); |  | ||||||
|  |  | ||||||
|     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)?; |  | ||||||
|             Ok(TcpListener::from_std(listener)?) |  | ||||||
|         } |  | ||||||
|         // otherwise fall back to local listening |  | ||||||
|         None => Ok(TcpListener::bind(DEFAULT_LISTENER).await?), |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| 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())?; |  | ||||||
|     for templates_path in templates_paths { |  | ||||||
|         watcher.watch(templates_path.as_path(), notify::RecursiveMode::Recursive)?; |  | ||||||
|     } |  | ||||||
|     Ok(livereload.request_predicate::<Body, NotHtmxPredicate>(NotHtmxPredicate)) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[tokio::main] | #[tokio::main] | ||||||
| async fn main() -> Result<(), AppError> { | async fn main() { | ||||||
|     let manifest_dir = env::var("CARGO_MANIFEST_DIR").map_err(|_| AppError::MissingEnvVar { |     let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); | ||||||
|         var: "CARGO_MANIFEST_DIR", |  | ||||||
|     })?; |  | ||||||
|     let assets_path = Path::new(&manifest_dir).join("assets"); |     let assets_path = Path::new(&manifest_dir).join("assets"); | ||||||
|     let templates_paths = vec![ |     let router = get_router(assets_path.as_path()); | ||||||
|         Path::new(&manifest_dir).join("src/pages"), |  | ||||||
|         Path::new(&manifest_dir).join("src/components"), |  | ||||||
|     ]; |  | ||||||
|  |  | ||||||
|     let livereload_layer = |     // TODO: select port based on available port (or ask in CLI) | ||||||
|         get_livereload_layer(templates_paths).map_err(AppError::NotifyWatcher)?; |     let listener = tokio::net::TcpListener::bind("localhost:3000").await.unwrap(); | ||||||
|     let router = get_router(assets_path.as_path()).layer(livereload_layer); |     println!("Listening on: http://{}", listener.local_addr().unwrap()); | ||||||
|  |     axum::serve(listener, router).await.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?; |  | ||||||
|     Ok(()) |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,23 +0,0 @@ | |||||||
| 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(), |  | ||||||
|         }, |  | ||||||
|     ] |  | ||||||
| } |  | ||||||
| @@ -1,43 +0,0 @@ | |||||||
| {% 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 %} |  | ||||||
| @@ -1,12 +1,10 @@ | |||||||
| use askama_axum::Template; | use askama::Template; | ||||||
| use axum_htmx::HxRequest; | use askama_axum::IntoResponse; | ||||||
|  |  | ||||||
| #[derive(Template)] | #[derive(Template)] | ||||||
| #[template(path = "cps.html")] | #[template(path = "pages/cps.html")] | ||||||
| pub struct CpsTemplate { | struct CpsResponse; | ||||||
|     hx_request: bool, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| pub async fn cps(HxRequest(hx_request): HxRequest) -> CpsTemplate { | pub async fn cps() -> impl IntoResponse { | ||||||
|     CpsTemplate { hx_request } |     CpsResponse.into_response() | ||||||
| } | } | ||||||
| @@ -1,43 +0,0 @@ | |||||||
| {% 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 %} |  | ||||||
| @@ -1,12 +1,10 @@ | |||||||
| use askama_axum::Template; | use askama::Template; | ||||||
| use axum_htmx::HxRequest; | use askama_axum::IntoResponse; | ||||||
|  |  | ||||||
| #[derive(Template)] | #[derive(Template)] | ||||||
| #[template(path = "home.html")] | #[template(path = "pages/home.html")] | ||||||
| pub struct GetHomeTemplate { | struct HomeResponse; | ||||||
|     hx_request: bool, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| pub async fn home(HxRequest(hx_request): HxRequest) -> GetHomeTemplate { | pub async fn home() -> impl IntoResponse { | ||||||
|     GetHomeTemplate { hx_request } |     HomeResponse.into_response() | ||||||
| } | } | ||||||
| @@ -5,6 +5,6 @@ mod home; | |||||||
|  |  | ||||||
| pub fn get_routes() -> Router { | pub fn get_routes() -> Router { | ||||||
|     Router::new() |     Router::new() | ||||||
|         .route("/", routing::get(home::home)) |         .route("/home", routing::get(home::home)) | ||||||
|         .route("/cps", routing::get(cps::cps)) |         .route("/cps", routing::get(cps::cps)) | ||||||
| } | } | ||||||
							
								
								
									
										20
									
								
								crates/app/src/templates/hello.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								crates/app/src/templates/hello.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | use askama::Template; | ||||||
|  | use askama_axum::IntoResponse; | ||||||
|  | use axum::{routing, Router}; | ||||||
|  |  | ||||||
|  | #[derive(Template)] | ||||||
|  | #[template(path = "hello.html")] | ||||||
|  | struct HelloResponse { | ||||||
|  |     pub name: String, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async fn hello() -> impl IntoResponse { | ||||||
|  |     HelloResponse { | ||||||
|  |         name: "Theo".to_string(), | ||||||
|  |     }.into_response() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn get_routes() -> Router { | ||||||
|  |     Router::new() | ||||||
|  |         .route("/", routing::get(hello)) | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								crates/app/src/templates/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								crates/app/src/templates/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | 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()) | ||||||
|  | } | ||||||
							
								
								
									
										60
									
								
								crates/app/src/templates/nav.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								crates/app/src/templates/nav.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | |||||||
|  | use askama::Template; | ||||||
|  | use askama_axum::IntoResponse; | ||||||
|  | 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 MenuResponse { | ||||||
|  |     mobile: bool, | ||||||
|  |     items: Vec<MenuItem>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl MenuResponse { | ||||||
|  |     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>) -> impl IntoResponse { | ||||||
|  |     MenuResponse { | ||||||
|  |         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, | ||||||
|  |             }, | ||||||
|  |         ],         | ||||||
|  |     }.into_response() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn get_routes() -> Router { | ||||||
|  |     Router::new() | ||||||
|  |         .route("/menu", routing::get(menu)) | ||||||
|  | } | ||||||
							
								
								
									
										65
									
								
								crates/app/src/templates/profile.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								crates/app/src/templates/profile.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | |||||||
|  | use askama::Template; | ||||||
|  | use askama_axum::IntoResponse; | ||||||
|  | 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 MenuResponse { | ||||||
|  |     mobile: bool, | ||||||
|  |     items: Vec<MenuItem>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl MenuResponse { | ||||||
|  |     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>) -> impl IntoResponse { | ||||||
|  |     MenuResponse { | ||||||
|  |         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, | ||||||
|  |             }, | ||||||
|  |         ], | ||||||
|  |     }.into_response() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn get_routes() -> Router { | ||||||
|  |     Router::new() | ||||||
|  |         .route("/menu", routing::get(menu)) | ||||||
|  | } | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| /** @type {import('tailwindcss').Config} */ | /** @type {import('tailwindcss').Config} */ | ||||||
| module.exports = { | module.exports = { | ||||||
|   content: [ |   content: [ | ||||||
|     './src/**/*.html', |     './templates/**/*.html', | ||||||
|     './css/**/*.css', |     './css/**/*.css', | ||||||
|   ], |   ], | ||||||
|   theme: { |   theme: { | ||||||
|   | |||||||
| @@ -1,7 +1,3 @@ | |||||||
| {% if hx_request %} |  | ||||||
|   <title>{% block title %}{{ title }}{% endblock %}</title> |  | ||||||
|   {% block body %}{% endblock %} |  | ||||||
| {% else %} |  | ||||||
| <!doctype html> | <!doctype html> | ||||||
| <html lang="fr" class="h-full"> | <html lang="fr" class="h-full"> | ||||||
|   <head> |   <head> | ||||||
| @@ -9,15 +5,19 @@ | |||||||
| 
 | 
 | ||||||
|     <script src="/assets/js/htmx@2.0.1.min.js"></script> |     <script src="/assets/js/htmx@2.0.1.min.js"></script> | ||||||
|     <script src="/assets/js/alpinejs@3.14.1.min.js" defer></script> |     <script src="/assets/js/alpinejs@3.14.1.min.js" defer></script> | ||||||
|     <script src="/assets/js/flowbite@2.5.1.min.js"></script> |  | ||||||
|     <link href="/assets/css/style.css" rel="stylesheet"> |     <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> | ||||||
| 
 | 
 | ||||||
|     {% block head %}{% endblock %} |     {% block head %}{% endblock %} | ||||||
|   </head> |   </head> | ||||||
|   <body class="h-full"> |   <body class="h-full"> | ||||||
|     <div class="min-h-full"> |     <div class="min-h-full"> | ||||||
|  |       {% block nav %} | ||||||
|  |         {% include "layout/nav.html" %} | ||||||
|  |       {% endblock %} | ||||||
|  | 
 | ||||||
|       {% block body %}{% endblock %} |       {% block body %}{% endblock %} | ||||||
|     </div> |     </div> | ||||||
|   </body> |   </body> | ||||||
| </html> | </html> | ||||||
| {% endif %} |  | ||||||
							
								
								
									
										1
									
								
								crates/app/templates/hello.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								crates/app/templates/hello.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <div>Hello {{name}}!</div> | ||||||
							
								
								
									
										34
									
								
								crates/app/templates/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								crates/app/templates/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | {% 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 %} | ||||||
							
								
								
									
										41
									
								
								crates/app/templates/layout/nav.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								crates/app/templates/layout/nav.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | |||||||
|  | <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> | ||||||
							
								
								
									
										9
									
								
								crates/app/templates/layout/nav/desktop/menu-items.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								crates/app/templates/layout/nav/desktop/menu-items.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | <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> | ||||||
| @@ -0,0 +1,6 @@ | |||||||
|  | <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> | ||||||
| @@ -0,0 +1,22 @@ | |||||||
|  | <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> | ||||||
							
								
								
									
										27
									
								
								crates/app/templates/layout/nav/desktop/profile.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								crates/app/templates/layout/nav/desktop/profile.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | <!-- 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> | ||||||
							
								
								
									
										4
									
								
								crates/app/templates/layout/nav/logo.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								crates/app/templates/layout/nav/logo.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | <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> | ||||||
							
								
								
									
										43
									
								
								crates/app/templates/layout/nav/mobile/menu-button.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								crates/app/templates/layout/nav/mobile/menu-button.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | |||||||
|  | <!-- 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> | ||||||
							
								
								
									
										9
									
								
								crates/app/templates/layout/nav/mobile/menu-items.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								crates/app/templates/layout/nav/mobile/menu-items.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | <div | ||||||
|  |   id="nav-menu-mobile" | ||||||
|  |   hx-get="/nav/menu?mobile=true" | ||||||
|  |   hx-target="this" | ||||||
|  |   hx-trigger="load" | ||||||
|  |   hx-swap="outerHTML" | ||||||
|  | > | ||||||
|  |   Chargement ... | ||||||
|  | </div> | ||||||
| @@ -0,0 +1,6 @@ | |||||||
|  | <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> | ||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | <div | ||||||
|  |   id="profile-menu-mobile" | ||||||
|  |   hx-get="/profile/menu?mobile=true" | ||||||
|  |   hx-target="this" | ||||||
|  |   hx-trigger="load" | ||||||
|  |   hx-swap="outerHTML" | ||||||
|  | > | ||||||
|  |   Chargement ... | ||||||
|  | </div> | ||||||
							
								
								
									
										11
									
								
								crates/app/templates/layout/nav/mobile/profile.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								crates/app/templates/layout/nav/mobile/profile.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | <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> | ||||||
							
								
								
									
										13
									
								
								crates/app/templates/layout/nav/nav-menu-items.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								crates/app/templates/layout/nav/nav-menu-items.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | {% 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 %} | ||||||
							
								
								
									
										16
									
								
								crates/app/templates/layout/nav/notifications-icon.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								crates/app/templates/layout/nav/notifications-icon.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | <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> | ||||||
							
								
								
									
										11
									
								
								crates/app/templates/layout/nav/profile-menu-items.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								crates/app/templates/layout/nav/profile-menu-items.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | {% 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 %} | ||||||
							
								
								
									
										52
									
								
								crates/app/templates/pages/cps.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								crates/app/templates/pages/cps.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | |||||||
|  | <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> | ||||||
							
								
								
									
										52
									
								
								crates/app/templates/pages/home.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								crates/app/templates/pages/home.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | |||||||
|  | <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> | ||||||
| @@ -21,5 +21,4 @@ tokio = "1.39.1" | |||||||
| app = { path = "../app" } | app = { path = "../app" } | ||||||
| http = "1.1.0" | http = "1.1.0" | ||||||
| bytes = "1.6.1" | bytes = "1.6.1" | ||||||
| thiserror = "1.0.63" |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,30 +1,21 @@ | |||||||
| use axum::body::{to_bytes, Body}; |  | ||||||
| use axum::Router; |  | ||||||
| use bytes::Bytes; | use bytes::Bytes; | ||||||
| use http::{request, response, Request, Response}; | use http::{request, response, Request, Response}; | ||||||
| use std::path::PathBuf; | use std::path::PathBuf; | ||||||
| use std::sync::Arc; | use std::sync::Arc; | ||||||
|  |  | ||||||
|  | use axum::body::{to_bytes, Body}; | ||||||
|  |  | ||||||
|  | use axum::Router; | ||||||
|  |  | ||||||
| use tauri::path::BaseDirectory; | use tauri::path::BaseDirectory; | ||||||
| use tauri::Manager; | use tauri::Manager; | ||||||
| use thiserror::Error; |  | ||||||
| use tokio::sync::{Mutex, MutexGuard}; | use tokio::sync::{Mutex, MutexGuard}; | ||||||
| use tower::{Service, ServiceExt}; | 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( | async fn process_tauri_request( | ||||||
|     tauri_request: Request<Vec<u8>>, |     tauri_request: Request<Vec<u8>>, | ||||||
|     mut router: MutexGuard<'_, Router>, |     mut router: MutexGuard<'_, Router>, | ||||||
| ) -> Result<Response<Vec<u8>>, DesktopError> { | ) -> Response<Vec<u8>> { | ||||||
|     let (parts, body): (request::Parts, Vec<u8>) = tauri_request.into_parts(); |     let (parts, body): (request::Parts, Vec<u8>) = tauri_request.into_parts(); | ||||||
|     let axum_request: Request<Body> = Request::from_parts(parts, body.into()); |     let axum_request: Request<Body> = Request::from_parts(parts, body.into()); | ||||||
|  |  | ||||||
| @@ -32,16 +23,17 @@ async fn process_tauri_request( | |||||||
|         .as_service() |         .as_service() | ||||||
|         .ready() |         .ready() | ||||||
|         .await |         .await | ||||||
|         .map_err(DesktopError::Infallible)? |         .expect("Failed to get ready service from router") | ||||||
|         .call(axum_request) |         .call(axum_request) | ||||||
|         .await |         .await | ||||||
|         .map_err(DesktopError::Infallible)?; |         .expect("Could not get response from router"); | ||||||
|  |  | ||||||
|     let (parts, body): (response::Parts, Body) = axum_response.into_parts(); |     let (parts, body): (response::Parts, Body) = axum_response.into_parts(); | ||||||
|     let body: Bytes = to_bytes(body, usize::MAX).await?; |     let body: Bytes = to_bytes(body, usize::MAX).await.unwrap_or_default(); | ||||||
|  |  | ||||||
|     let tauri_response: Response<Vec<u8>> = Response::from_parts(parts, body.into()); |     let tauri_response: Response<Vec<u8>> = Response::from_parts(parts, body.into()); | ||||||
|     Ok(tauri_response) |  | ||||||
|  |     tauri_response | ||||||
| } | } | ||||||
|  |  | ||||||
| #[cfg_attr(mobile, tauri::mobile_entry_point)] | #[cfg_attr(mobile, tauri::mobile_entry_point)] | ||||||
| @@ -51,7 +43,7 @@ pub fn run() { | |||||||
|             let assets_path: PathBuf = app |             let assets_path: PathBuf = app | ||||||
|                 .path() |                 .path() | ||||||
|                 .resolve("assets", BaseDirectory::Resource) |                 .resolve("assets", BaseDirectory::Resource) | ||||||
|                 .expect("Assets path should be resolvable"); |                 .expect("Path should be resolvable"); | ||||||
|  |  | ||||||
|             // Adds Axum router to application state |             // Adds Axum router to application state | ||||||
|             // This makes it so we can retrieve it from any app instance (see bellow) |             // This makes it so we can retrieve it from any app instance (see bellow) | ||||||
| @@ -68,19 +60,8 @@ pub fn run() { | |||||||
|             // Spawn a new async task to process the request |             // Spawn a new async task to process the request | ||||||
|             tauri::async_runtime::spawn(async move { |             tauri::async_runtime::spawn(async move { | ||||||
|                 let router = router.lock().await; |                 let router = router.lock().await; | ||||||
|                 match process_tauri_request(request, router).await { |                 let response = process_tauri_request(request, router).await; | ||||||
|                     Ok(response) => responder.respond(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!()) |         .run(tauri::generate_context!()) | ||||||
|   | |||||||
| @@ -2,10 +2,8 @@ | |||||||
| name = "services-sesam-vitale-sys" | name = "services-sesam-vitale-sys" | ||||||
| version = "0.1.0" | version = "0.1.0" | ||||||
| edition = "2021" | edition = "2021" | ||||||
| #links= "ssvlux64" |  | ||||||
|  |  | ||||||
| [dependencies] | [dependencies] | ||||||
| bitvec = "1.0.1" | bitvec = "1.0.1" | ||||||
| deku = "0.17.0" | deku = "0.17.0" | ||||||
| libc = "0.2.155" | libc = "0.2.155" | ||||||
| thiserror = "1.0.63" |  | ||||||
|   | |||||||
| @@ -1,66 +0,0 @@ | |||||||
| use thiserror::Error; |  | ||||||
| use std::{ffi::CString, fmt, path::Path, ptr}; |  | ||||||
|  |  | ||||||
| use crate::{bindings::{SSV_InitLIB2, SSV_TermLIB}, types::{common::read_from_buffer, configuration::Configuration}}; |  | ||||||
|  |  | ||||||
| #[derive(Error, Debug)] |  | ||||||
| pub struct SesamVitaleError { |  | ||||||
|     code: u16, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| impl fmt::Display for SesamVitaleError { |  | ||||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |  | ||||||
|         write!(f, "Got error code {} from SSV_LireConfig", self.code) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| pub fn init_library(sesam_ini_path: &Path) -> Result<(), SesamVitaleError> { |  | ||||||
|     // TODO: better error handling |  | ||||||
|     let path_str = sesam_ini_path.to_str().unwrap(); |  | ||||||
|     let path_ptr = CString::new(path_str).expect("failed to create cstring"); |  | ||||||
|  |  | ||||||
|     let exit_code: u16 = unsafe { SSV_InitLIB2(path_ptr.as_ptr()) }; |  | ||||||
|     if exit_code != 0 { |  | ||||||
|         let error = SesamVitaleError { code: exit_code }; |  | ||||||
|         return Err(error); |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     Ok(()) |  | ||||||
| } |  | ||||||
| pub fn close_library() -> Result<(), SesamVitaleError> { |  | ||||||
|     let exit_code: u16 = unsafe { SSV_TermLIB() }; |  | ||||||
|     if exit_code != 0 { |  | ||||||
|         let error = SesamVitaleError { code: exit_code }; |  | ||||||
|         return Err(error); |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     Ok(()) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| pub fn read_config() -> Result<Configuration, SesamVitaleError> { |  | ||||||
|     let mut buffer_ptr: *mut libc::c_void = ptr::null_mut(); |  | ||||||
|     let mut size: libc::size_t = 0; |  | ||||||
|  |  | ||||||
|     let buffer_ptr_ptr: *mut *mut libc::c_void = &mut buffer_ptr; |  | ||||||
|     let size_ptr: *mut libc::size_t = &mut size; |  | ||||||
|  |  | ||||||
|     // Need to add proper error handling -> return a result with error code pointing to an error |  | ||||||
|     // enum |  | ||||||
|     let exit_code: u16 = unsafe { SSV_LireConfig(buffer_ptr_ptr, size_ptr) }; |  | ||||||
|  |  | ||||||
|     if exit_code != 0 { |  | ||||||
|         let error = SesamVitaleError { code: exit_code }; |  | ||||||
|         return Err(error); |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     let buffer: &[u8] = unsafe { std::slice::from_raw_parts(buffer_ptr as *const u8, size) }; |  | ||||||
|  |  | ||||||
|     // TODO: Improve error handling |  | ||||||
|     let configuration: Configuration = read_from_buffer(buffer).unwrap(); |  | ||||||
|  |  | ||||||
|     // TODO: Call library function for memory delocating |  | ||||||
|     unsafe { libc::free(buffer_ptr) }; |  | ||||||
|  |  | ||||||
|     Ok(configuration) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @@ -1,6 +1,70 @@ | |||||||
| pub mod api; |  | ||||||
| mod bindings; | mod bindings; | ||||||
| pub mod types; | pub mod types; | ||||||
|  |  | ||||||
|  | use bindings::{SSV_InitLIB2, SSV_LireConfig, SSV_TermLIB}; | ||||||
|  | use std::{ffi::CString, fmt, path::Path, ptr}; | ||||||
|  | use types::serialization_types::{read_from_buffer, Configuration}; | ||||||
|  |  | ||||||
|  | #[derive(Debug)] | ||||||
|  | pub struct SesamVitaleError { | ||||||
|  |     code: u16, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl fmt::Display for SesamVitaleError { | ||||||
|  |     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||||
|  |         write!(f, "Got error code {} from SSV_LireConfig", self.code) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn init_library(sesam_ini_path: &Path) -> Result<(), SesamVitaleError> { | ||||||
|  |     // TODO: better error handling | ||||||
|  |     let path_str = sesam_ini_path.to_str().unwrap(); | ||||||
|  |     let path_ptr = CString::new(path_str).expect("failed to create cstring"); | ||||||
|  |  | ||||||
|  |     let exit_code: u16 = unsafe { SSV_InitLIB2(path_ptr.as_ptr()) }; | ||||||
|  |     if exit_code != 0 { | ||||||
|  |         let error = SesamVitaleError { code: exit_code }; | ||||||
|  |         return Err(error); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
|  | pub fn close_library() -> Result<(), SesamVitaleError> { | ||||||
|  |     let exit_code: u16 = unsafe { SSV_TermLIB() }; | ||||||
|  |     if exit_code != 0 { | ||||||
|  |         let error = SesamVitaleError { code: exit_code }; | ||||||
|  |         return Err(error); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn read_config() -> Result<Configuration, SesamVitaleError> { | ||||||
|  |     let mut buffer_ptr: *mut libc::c_void = ptr::null_mut(); | ||||||
|  |     let mut size: libc::size_t = 0; | ||||||
|  |  | ||||||
|  |     let buffer_ptr_ptr: *mut *mut libc::c_void = &mut buffer_ptr; | ||||||
|  |     let size_ptr: *mut libc::size_t = &mut size; | ||||||
|  |  | ||||||
|  |     // Need to add proper error handling -> return a result with error code pointing to an error | ||||||
|  |     // enum | ||||||
|  |     let exit_code: u16 = unsafe { SSV_LireConfig(buffer_ptr_ptr, size_ptr) }; | ||||||
|  |  | ||||||
|  |     if exit_code != 0 { | ||||||
|  |         let error = SesamVitaleError { code: exit_code }; | ||||||
|  |         return Err(error); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     let buffer: &[u8] = unsafe { std::slice::from_raw_parts(buffer_ptr as *const u8, size) }; | ||||||
|  |  | ||||||
|  |     // TODO: Improve error handling | ||||||
|  |     let configuration: Configuration = read_from_buffer(buffer).unwrap(); | ||||||
|  |  | ||||||
|  |     // TODO: Call library function for memory delocating | ||||||
|  |     unsafe { libc::free(buffer_ptr) }; | ||||||
|  |  | ||||||
|  |     Ok(configuration) | ||||||
|  | } | ||||||
|  |  | ||||||
| #[cfg(test)] | #[cfg(test)] | ||||||
| mod tests {} | mod tests {} | ||||||
|   | |||||||
| @@ -1,144 +1,264 @@ | |||||||
| use crate::types::configuration::{ | pub struct Identification<T> { | ||||||
|     ConfigurationHeader, PCSCReader, ReaderConfiguration, SESAMVitaleComponent, |     value: T, | ||||||
| }; |     // Key to check the validity of the value | ||||||
|  |     // TODO: implement checking algorithm | ||||||
| use std::{error::Error, str::FromStr}; |     key: u8, | ||||||
|  |  | ||||||
| use bitvec::index::BitIdx; |  | ||||||
| use deku::{ |  | ||||||
|     bitvec::{BitStore, Msb0}, |  | ||||||
|     ctx::ByteSize, |  | ||||||
|     deku_derive, |  | ||||||
|     reader::{Reader, ReaderRet}, |  | ||||||
|     DekuContainerRead, DekuError, DekuReader, |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| #[deku_derive(DekuRead)] |  | ||||||
| #[derive(Debug, Clone, PartialEq)] |  | ||||||
| pub(crate) struct NumericString(#[deku(map = "convert_from_data_field")] String); |  | ||||||
|  |  | ||||||
| #[deku_derive(DekuRead)] |  | ||||||
| #[derive(Debug, Clone, PartialEq)] |  | ||||||
| pub(crate) struct AlphaNumericString(#[deku(map = "convert_from_data_field")] String); |  | ||||||
|  |  | ||||||
| #[deku_derive(DekuRead)] |  | ||||||
| #[derive(Debug, Clone, PartialEq)] |  | ||||||
| pub(crate) struct BinaryData(#[deku(map = "extract_from_data_field")] Vec<u8>); |  | ||||||
|  |  | ||||||
| #[deku_derive(DekuRead)] |  | ||||||
| #[derive(Debug, Clone, Copy, PartialEq)] |  | ||||||
| #[deku(endian = "big")] |  | ||||||
| pub(crate) struct GroupId(u16); |  | ||||||
|  |  | ||||||
| trait MapToDekuParseError<T> { |  | ||||||
|     fn map_to_deku_parse_error(self) -> Result<T, DekuError>; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| impl<T, E: Error> MapToDekuParseError<T> for Result<T, E> { | pub type Byte = u8; | ||||||
|     fn map_to_deku_parse_error(self) -> Result<T, DekuError> { |  | ||||||
|         self.map_err(|e| DekuError::Parse(e.to_string().into())) | pub(crate) enum IdentificationNationale { | ||||||
|     } |     NumeroAdeli(String), | ||||||
|  |     NumeroEmployeeDansStructure(IdentificationStructure, String), | ||||||
|  |     NumeroDRASS(String), | ||||||
|  |     NumeroRPPS(String), | ||||||
|  |     /// N° Etudiant Médecin type ADELI sur 9 caractères (information transmise par l’ANS) | ||||||
|  |     NumeroEtudiantMedecin(String), | ||||||
| } | } | ||||||
|  |  | ||||||
| fn read_size<R: std::io::Read>(reader: &mut Reader<R>) -> Result<ByteSize, DekuError> { | pub(crate) enum TypeCarteProfessionnelSante { | ||||||
|     let first_byte: u8 = u8::from_reader_with_ctx(reader, ())?; |     /// Carte de Professionnel de Santé (CPS) | ||||||
|  |     CarteDeProfessionnelSante, | ||||||
|     let is_length_expanded = first_byte.get_bit::<Msb0>(BitIdx::new(0).map_to_deku_parse_error()?); |     /// Carte de Professionnel de Santé en Formation (CPF) | ||||||
|  |     CarteDeProfessionnelSanteEnFormation, | ||||||
|     match is_length_expanded { |     /// Carte de Personnel d'Établissement de Santé (CDE/CPE) | ||||||
|         true => { |     CarteDePersonnelEtablissementSante, | ||||||
|             let size_of_data_size: ByteSize = ByteSize((first_byte & 0b0111_1111) as usize); |     /// Carte de Personnel Autorisé (CDA/CPA) | ||||||
|  |     CarteDePersonnelAutorise, | ||||||
|             if size_of_data_size.0 > 4 { |     /// Carte de Personne Morale | ||||||
|                 return Err(DekuError::Parse("Size of the length encoding is > 4, this is not normal. Probable parsing error".to_string().into())); |     CarteDePersonneMorale, | ||||||
|             }; |  | ||||||
|  |  | ||||||
|             // maximum size of the buffer is 4, we use the offset to read values less than 4 bytes |  | ||||||
|             let buffer: &mut [u8; 4] = &mut [0; 4]; |  | ||||||
|             let write_offset = 4 - size_of_data_size.0; |  | ||||||
|  |  | ||||||
|             match reader.read_bytes(size_of_data_size.0, &mut buffer[write_offset..])? { |  | ||||||
|                 ReaderRet::Bits(_bit_vec) => Err(DekuError::Parse("Got bits when trying to read bytes -> reader is unaligned, this is not normal.".to_string().into())), |  | ||||||
|                 ReaderRet::Bytes => Ok(ByteSize(u32::from_be_bytes(*buffer) as usize)), |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         false => Ok(ByteSize(first_byte as usize)), |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // Using this as the map function asks deku to parse a datafield | pub(crate) enum CategorieCarteProfessionnelSante { | ||||||
| // We then use the datafield and convert it to the corresponding value |     Reelle, | ||||||
| pub(super) fn convert_from_data_field<T>(data_field: DataField) -> Result<T, DekuError> |     Test, | ||||||
| where |     Demonstration, | ||||||
|     T: FromStr, |  | ||||||
|     T::Err: Error, |  | ||||||
| { |  | ||||||
|     let text = String::from_utf8(data_field.data).map_to_deku_parse_error()?; |  | ||||||
|     T::from_str(&text).map_to_deku_parse_error() |  | ||||||
| } | } | ||||||
|  |  | ||||||
| pub(crate) fn extract_from_data_field(data_field: DataField) -> Result<Vec<u8>, DekuError> { | pub(crate) enum CodeCivilite { | ||||||
|     Ok(data_field.data) |     Adjudant, | ||||||
|  |     Amiral, | ||||||
|  |     Aspirant, | ||||||
|  |     Aumônier, | ||||||
|  |     Capitaine, | ||||||
|  |     Cardinal, | ||||||
|  |     Chanoine, | ||||||
|  |     Colonel, | ||||||
|  |     Commandant, | ||||||
|  |     Commissaire, | ||||||
|  |     Conseiller, | ||||||
|  |     Directeur, | ||||||
|  |     Docteur, | ||||||
|  |     Douanier, | ||||||
|  |     Epouxse, // Epoux(se) | ||||||
|  |     Evêque, | ||||||
|  |     Général, | ||||||
|  |     Gouverneur, | ||||||
|  |     Ingénieur, | ||||||
|  |     Inspecteur, | ||||||
|  |     Lieutenant, | ||||||
|  |     Madame, | ||||||
|  |     Mademoiselle, | ||||||
|  |     Maître, | ||||||
|  |     Maréchal, | ||||||
|  |     Médecin, | ||||||
|  |     Mesdames, | ||||||
|  |     Mesdemoiselles, | ||||||
|  |     Messieurs, | ||||||
|  |     Monseigneur, | ||||||
|  |     Monsieur, | ||||||
|  |     NotreDame, | ||||||
|  |     Pasteur, | ||||||
|  |     Préfet, | ||||||
|  |     Président, | ||||||
|  |     Professeur, | ||||||
|  |     Recteur, | ||||||
|  |     Sergent, | ||||||
|  |     SousPréfet, | ||||||
|  |     Technicien, | ||||||
|  |     Veuve, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[deku_derive(DekuRead)] | pub(crate) enum IdentificationStructure { | ||||||
| #[derive(Debug, PartialEq)] |     NumeroAdeliCabinet(String), | ||||||
| pub(crate) struct DataField { |     NumeroFINESS(String), | ||||||
|     #[deku(reader = "read_size(deku::reader)")] |     NumeroSIREN(String), | ||||||
|     pub(crate) data_size: ByteSize, |     NumeroSIRET(String), | ||||||
|  |     NumeroRPPSCabinet(String), | ||||||
|     #[deku(bytes_read = "data_size.0")] |  | ||||||
|     pub(crate) data: Vec<u8>, |  | ||||||
| } | } | ||||||
|  |  | ||||||
| #[deku_derive(DekuRead)] | pub(crate) enum ModeExercice { | ||||||
| #[derive(Debug, PartialEq)] |     LiberalExploitantCommercant, // Libéral, exploitant, commerçant | ||||||
| pub(crate) struct BlockHeader { |     Salarie, | ||||||
|     pub(crate) group_id: GroupId, |     Remplacant, | ||||||
|  |     Benevole, | ||||||
|     #[deku(reader = "read_size(deku::reader)")] |  | ||||||
|     pub(crate) data_size: ByteSize, |  | ||||||
| } | } | ||||||
|  |  | ||||||
| #[deku_derive(DekuRead)] | pub(crate) enum StatutExercice { | ||||||
| #[derive(Debug, PartialEq)] |     // TAB-Statuts géré par l’ANS il faut trouver la donnee | ||||||
| pub(crate) struct DataBlock { |     PLACEHOLDER(u8), | ||||||
|     pub(crate) header: BlockHeader, |  | ||||||
|  |  | ||||||
|     #[deku(ctx = "header.group_id")] |  | ||||||
|     pub(crate) inner: DataGroup, |  | ||||||
| } | } | ||||||
|  |  | ||||||
| #[deku_derive(DekuRead)] | pub(crate) enum SecteurActivite { | ||||||
| #[derive(Debug, PartialEq)] |     EtablissementPublicDeSanté, | ||||||
| #[deku(ctx = "group_id: GroupId", id = "group_id.0")] |     HopitauxMilitaires, | ||||||
| pub enum DataGroup { |     EtablissementPrivePSPH, // Participant au Service Public Hospitalier | ||||||
|     #[deku(id = 60)] |     EtablissementPriveNonPSPH, | ||||||
|     ConfigurationHeader(ConfigurationHeader), |     DispensaireDeSoins, | ||||||
|     #[deku(id = 61)] |     AutresStructuresDeSoinsRelevantDuServiceDeSanteDesArmees, | ||||||
|     ReaderConfiguration(ReaderConfiguration), |     CabinetIndividuel, | ||||||
|     #[deku(id = 64)] |     CabinetDeGroupe, | ||||||
|     SESAMVitaleComponent(SESAMVitaleComponent), |     ExerciceEnSociete, | ||||||
|     #[deku(id = 67)] |     SecteurPrivePHTempsPlein, | ||||||
|     PCSCReader(PCSCReader), |     TransportSanitaire, | ||||||
|  |     EntrepriseDInterim, | ||||||
|  |     EtablissementDeSoinsEtPrevention, | ||||||
|  |     PreventionEtSoinsEnEntreprise, | ||||||
|  |     SanteScolaireEtUniversitaire, | ||||||
|  |     RecrutementEtGestionRH, | ||||||
|  |     PMIPlanificationFamiliale, | ||||||
|  |     EtablissementPourHandicapes, | ||||||
|  |     ComMarketingConsultingMedia, | ||||||
|  |     EtablissementPersonnesAgees, | ||||||
|  |     EtablissementAideaLaFamille, | ||||||
|  |     EtablissementDEnseignement, | ||||||
|  |     EtablissementsDeProtectionDeLEnfance, | ||||||
|  |     EtablissementsDHebergementEtDeReadaptation, | ||||||
|  |     Recherche, | ||||||
|  |     AssurancePrivee, | ||||||
|  |     OrganismeDeSecuriteSociale, | ||||||
|  |     MinistèreEtServicesDeconcentres, | ||||||
|  |     CollectivitesTerritoriales, | ||||||
|  |     AssociationsEtOrganitationsHumanitaire, | ||||||
|  |     LaboratoireDeBiologieMedicale, | ||||||
|  |     AutreEtablissementSanitaire, | ||||||
|  |     ProductionCommercialisationGrosBienMedicaux, | ||||||
|  |     CommerceDétailDeBiensMédicaux, | ||||||
|  |     PharmacieDOfficine, | ||||||
|  |     CentreDeDialyse, | ||||||
|  |     ParaPharmacie, | ||||||
|  |     AutreSecteurDActivité, | ||||||
|  |     SecteurNonDefini, | ||||||
|  |     CentreAntiCancer, | ||||||
|  |     CentreDeTransfusionSanguine, | ||||||
|  |     RépartitionDistribributionFabricationExploitationImportationMedicamentsEtDispositifsMédicaux, | ||||||
|  |     IncendiesEtSecours, | ||||||
|  |     EntreprisesIndustriellesEtTertiairesHorsIndustriesPharmaceutiques, | ||||||
|  |     EntiteDUnTOM, | ||||||
|  |     FabricationExploitationImportationMedicamentsEtDispositifsMedicaux, | ||||||
| } | } | ||||||
| pub(crate) fn read_from_buffer<T>(buffer: &[u8]) -> Result<T, T::Error> |  | ||||||
| where |  | ||||||
|     T: TryFrom<Vec<DataBlock>>, |  | ||||||
| { |  | ||||||
|     let mut data_blocks: Vec<DataBlock> = Vec::new(); |  | ||||||
|     let mut offset = 0; |  | ||||||
|  |  | ||||||
|     let mut remaining_buffer = buffer; | pub(crate) type IdentificationFacturation = u32; | ||||||
|  | pub(crate) enum CodeConventionnel { | ||||||
|     while !remaining_buffer.is_empty() { |     NonConventionne, | ||||||
|         // TODO: properly handle errors |     Conventionne, | ||||||
|         let (rest, data_block) = DataBlock::from_bytes((remaining_buffer, offset)).unwrap(); |     ConventionneAvecDepassement, | ||||||
|  |     ConventionneAvecHonorairesLibres, | ||||||
|         data_blocks.push(data_block); | } | ||||||
|  |  | ||||||
|         (remaining_buffer, offset) = rest; | /// Code spécialité ou Code spécialité de l'exécutant | ||||||
|     } | pub(crate) enum CodeSpecialite { | ||||||
|  |     MedecineGenerale, | ||||||
|     T::try_from(data_blocks) |     AnesthesieReanimation, | ||||||
|  |     Cardiologie, | ||||||
|  |     ChirurgieGenerale, | ||||||
|  |     DermatologieEtVenerologie, | ||||||
|  |     Radiologie, | ||||||
|  |     GynecologieObstetrique, | ||||||
|  |     GastroEnterologieEtHepatologie, | ||||||
|  |     MedecineInterne, | ||||||
|  |     NeuroChirurgie, | ||||||
|  |     OtoRhinoLaryngologie, | ||||||
|  |     Pediatrie, | ||||||
|  |     Pneumologie, | ||||||
|  |     Rhumatologie, | ||||||
|  |     Ophtalmologie, | ||||||
|  |     ChirurgieUrologique, | ||||||
|  |     NeuroPsychiatrie, | ||||||
|  |     Stomatologie, | ||||||
|  |     ChirurgienDentiste, | ||||||
|  |     ReanimationMedicale, | ||||||
|  |     SageFemme, | ||||||
|  |     SpecialisteEnMedecineGeneraleAvecDiplome, | ||||||
|  |     SpecialisteEnMedecineGeneraleReconnuParLOrdre, | ||||||
|  |     Infirmier, | ||||||
|  |     Psychologue, | ||||||
|  |     MasseurKinesitherapeute, | ||||||
|  |     PedicurePodologue, | ||||||
|  |     Orthophoniste, | ||||||
|  |     Orthoptiste, | ||||||
|  |     LaboratoireDAnalysesMedicales, | ||||||
|  |     ReeducationReadaptationFonctionnelle, | ||||||
|  |     Neurologie, | ||||||
|  |     Psychiatrie, | ||||||
|  |     Geriatrie, | ||||||
|  |     Nephrologie, | ||||||
|  |     ChirurgieDentaireSpecialiteODF, | ||||||
|  |     AnatomoCytoPathologie, | ||||||
|  |     MedecinBiologiste, | ||||||
|  |     LaboratoirePolyvalent, | ||||||
|  |     LaboratoireDAnatomoCytoPathologique, | ||||||
|  |     ChirurgieOrthopediqueEtTraumatologie, | ||||||
|  |     EndocrinologieEtMetabolisme, | ||||||
|  |     ChirurgieInfantile, | ||||||
|  |     ChirurgieMaxilloFaciale, | ||||||
|  |     ChirurgieMaxilloFacialeEtStomatologie, | ||||||
|  |     ChirurgiePlastiqueReconstructriceEtEsthetique, | ||||||
|  |     ChirurgieThoraciqueEtCardioVasculaire, | ||||||
|  |     ChirurgieVasculaire, | ||||||
|  |     ChirurgieVisceraleEtDigestive, | ||||||
|  |     PharmacieDOfficine, | ||||||
|  |     PharmacieMutualiste, | ||||||
|  |     ChirurgienDentisteSpecialiteCO, | ||||||
|  |     ChirurgienDentisteSpecialiteMBD, | ||||||
|  |     PrestataireDeTypeSociete, | ||||||
|  |     PrestataireArtisan, | ||||||
|  |     PrestataireDeTypeAssociation, | ||||||
|  |     Orthesiste, | ||||||
|  |     Opticien, | ||||||
|  |     Audioprothesiste, | ||||||
|  |     ÉpithesisteOculariste, | ||||||
|  |     PodoOrthesiste, | ||||||
|  |     Orthoprothesiste, | ||||||
|  |     ChirurgieOrale, | ||||||
|  |     GynecologieMedicale, | ||||||
|  |     Hematologie, | ||||||
|  |     MedecineNucleaire, | ||||||
|  |     OncologieMedicale, | ||||||
|  |     OncologieRadiotherapique, | ||||||
|  |     PsychiatrieDeLEnfantEtDeLAdolescent, | ||||||
|  |     Radiotherapie, | ||||||
|  |     Obstetrique, | ||||||
|  |     GenetiqueMedicale, | ||||||
|  |     ObstetriqueEtGynecologieMedicale, | ||||||
|  |     SantePubliqueEtMedecineSociale, | ||||||
|  |     MedecineDesMaladiesInfectieusesEtTropicales, | ||||||
|  |     MedecineLegaleEtExpertisesMedicales, | ||||||
|  |     MedecineDUrgence, | ||||||
|  |     MedecineVasculaire, | ||||||
|  |     Allergologie, | ||||||
|  |     InfirmierExercantEnPratiquesAvancees, // IPA | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// Page 54 dictionnaires des donnees | ||||||
|  | /// donnees inutilises pour les pharmacies | ||||||
|  | pub(crate) enum CodeZoneTarifaire {} | ||||||
|  |  | ||||||
|  | pub(crate) enum CodeZoneIK { | ||||||
|  |     PasIndemniteKilometrique, | ||||||
|  |     IndemnitesKilometriquesPlaine, | ||||||
|  |     IndemnitesKilometriquesMontagne, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub(crate) enum CodeAgrement { | ||||||
|  |     PasDAgrementRadio, | ||||||
|  |     /// Agrément D ou agrément DDASS | ||||||
|  |     AgrementDDASS, | ||||||
|  |     /// Agrément A, B, C, E et F | ||||||
|  |     AgrementABCEF, | ||||||
|  |     /// Agrément G, H et J | ||||||
|  |     AgrementGHJ, | ||||||
|  |     AgrementK, | ||||||
|  |     AgrementL, | ||||||
|  |     AgrementM, | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,137 +0,0 @@ | |||||||
| use crate::types::common::DataBlock; |  | ||||||
| use std::{error::Error, fmt, vec::Vec}; |  | ||||||
|  |  | ||||||
| use crate::types::common::convert_from_data_field; |  | ||||||
| use deku::{deku_derive, DekuReader}; |  | ||||||
|  |  | ||||||
| use super::common::{AlphaNumericString, DataGroup}; |  | ||||||
|  |  | ||||||
| #[deku_derive(DekuRead)] |  | ||||||
| #[derive(Debug, PartialEq)] |  | ||||||
| pub struct SSVVersionNumber(#[deku(map = "convert_from_data_field")] u16); |  | ||||||
|  |  | ||||||
| #[deku_derive(DekuRead)] |  | ||||||
| #[derive(Debug, PartialEq)] |  | ||||||
| pub struct GALSSVersionNumber(#[deku(map = "convert_from_data_field")] u16); |  | ||||||
|  |  | ||||||
| #[deku_derive(DekuRead)] |  | ||||||
| #[derive(Debug, PartialEq)] |  | ||||||
| pub struct PSSVersionNumber(#[deku(map = "convert_from_data_field")] u16); |  | ||||||
|  |  | ||||||
| #[deku_derive(DekuRead)] |  | ||||||
| #[derive(Debug, PartialEq)] |  | ||||||
| pub struct ConfigurationHeader { |  | ||||||
|     pub ssv_version: SSVVersionNumber, |  | ||||||
|     pub galss_version: GALSSVersionNumber, |  | ||||||
|     pub pss_version: PSSVersionNumber, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[deku_derive(DekuRead)] |  | ||||||
| #[derive(Debug, PartialEq)] |  | ||||||
| pub struct PCSCReaderName(AlphaNumericString); |  | ||||||
|  |  | ||||||
| #[deku_derive(DekuRead)] |  | ||||||
| #[derive(Debug, PartialEq)] |  | ||||||
| pub struct CardType(#[deku(map = "convert_from_data_field")] u8); |  | ||||||
|  |  | ||||||
| #[deku_derive(DekuRead)] |  | ||||||
| #[derive(Debug, PartialEq)] |  | ||||||
| pub struct PCSCReader { |  | ||||||
|     pub name: PCSCReaderName, |  | ||||||
|     pub card_type: CardType, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[deku_derive(DekuRead)] |  | ||||||
| #[derive(Debug, PartialEq)] |  | ||||||
| pub struct SESAMVitaleComponentID(#[deku(map = "convert_from_data_field")] u16); |  | ||||||
|  |  | ||||||
| #[deku_derive(DekuRead)] |  | ||||||
| #[derive(Debug, PartialEq)] |  | ||||||
| pub struct SESAMVitaleComponentDescription(AlphaNumericString); |  | ||||||
|  |  | ||||||
| #[deku_derive(DekuRead)] |  | ||||||
| #[derive(Debug, PartialEq)] |  | ||||||
| pub struct SESAMVitaleComponentVersion(AlphaNumericString); |  | ||||||
|  |  | ||||||
| #[deku_derive(DekuRead)] |  | ||||||
| #[derive(Debug, PartialEq)] |  | ||||||
| pub struct SESAMVitaleComponent { |  | ||||||
|     pub id: SESAMVitaleComponentID, |  | ||||||
|     pub description: SESAMVitaleComponentDescription, |  | ||||||
|     pub version: SESAMVitaleComponentVersion, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[deku_derive(DekuRead)] |  | ||||||
| #[derive(Debug, PartialEq)] |  | ||||||
| pub struct ReaderConfiguration {} |  | ||||||
|  |  | ||||||
| #[derive(Debug)] |  | ||||||
| pub enum ConfigurationError { |  | ||||||
|     MultipleConfigurationHeaders, |  | ||||||
|     MissingConfigurationHeader, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| impl fmt::Display for ConfigurationError { |  | ||||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |  | ||||||
|         match self { |  | ||||||
|             ConfigurationError::MultipleConfigurationHeaders => { |  | ||||||
|                 write!(f, "Multiple ConfigurationHeader blocks found") |  | ||||||
|             } |  | ||||||
|             ConfigurationError::MissingConfigurationHeader => { |  | ||||||
|                 write!(f, "Missing ConfigurationHeader block") |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| impl Error for ConfigurationError {} |  | ||||||
|  |  | ||||||
| #[derive(Debug)] |  | ||||||
| pub struct Configuration { |  | ||||||
|     pub configuration_header: ConfigurationHeader, |  | ||||||
|     pub reader_configurations: Vec<ReaderConfiguration>, |  | ||||||
|     pub sesam_vitale_components: Vec<SESAMVitaleComponent>, |  | ||||||
|     pub pcsc_readers: Vec<PCSCReader>, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| impl TryFrom<Vec<DataBlock>> for Configuration { |  | ||||||
|     type Error = ConfigurationError; |  | ||||||
|  |  | ||||||
|     fn try_from(data_blocks: Vec<DataBlock>) -> Result<Self, Self::Error> { |  | ||||||
|         let mut configuration_header: Option<ConfigurationHeader> = None; |  | ||||||
|         let mut reader_configurations: Vec<ReaderConfiguration> = Vec::new(); |  | ||||||
|         let mut sesam_vitale_components: Vec<SESAMVitaleComponent> = Vec::new(); |  | ||||||
|         let mut pcsc_readers: Vec<PCSCReader> = Vec::new(); |  | ||||||
|  |  | ||||||
|         for block in data_blocks { |  | ||||||
|             match block.inner { |  | ||||||
|                 DataGroup::ConfigurationHeader(header) => { |  | ||||||
|                     if configuration_header.is_some() { |  | ||||||
|                         return Err(ConfigurationError::MultipleConfigurationHeaders); |  | ||||||
|                     } |  | ||||||
|                     configuration_header = Some(header); |  | ||||||
|                 } |  | ||||||
|                 DataGroup::ReaderConfiguration(configuration) => { |  | ||||||
|                     reader_configurations.push(configuration) |  | ||||||
|                 } |  | ||||||
|                 DataGroup::SESAMVitaleComponent(component) => { |  | ||||||
|                     sesam_vitale_components.push(component); |  | ||||||
|                 } |  | ||||||
|                 DataGroup::PCSCReader(reader) => { |  | ||||||
|                     pcsc_readers.push(reader); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         let configuration_header = match configuration_header { |  | ||||||
|             Some(header) => header, |  | ||||||
|             None => return Err(ConfigurationError::MissingConfigurationHeader), |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         Ok(Self { |  | ||||||
|             configuration_header, |  | ||||||
|             reader_configurations, |  | ||||||
|             sesam_vitale_components, |  | ||||||
|             pcsc_readers, |  | ||||||
|         }) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,3 +1 @@ | |||||||
| pub mod common; | pub mod serialization_types; | ||||||
| pub mod configuration; |  | ||||||
| pub mod droits_vitale; |  | ||||||
|   | |||||||
| @@ -0,0 +1,248 @@ | |||||||
|  | use bitvec::index::BitIdx; | ||||||
|  | use std::{error::Error, fmt, str::FromStr, vec::Vec}; | ||||||
|  |  | ||||||
|  | use deku::{ | ||||||
|  |     bitvec::{BitStore, Msb0}, ctx::ByteSize, deku_derive, reader::{Reader, ReaderRet}, DekuContainerRead, DekuError, DekuReader | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | #[deku_derive(DekuRead)] | ||||||
|  | #[derive(Debug, Clone, Copy, PartialEq)] | ||||||
|  | #[deku(endian = "big")] | ||||||
|  | pub(crate) struct GroupId(u16); | ||||||
|  |  | ||||||
|  | trait MapToDekuParseError<T> { | ||||||
|  |     fn map_to_deku_parse_error(self) -> Result<T, DekuError>; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl<T, E: Error> MapToDekuParseError<T> for Result<T, E> { | ||||||
|  |     fn map_to_deku_parse_error(self) -> Result<T, DekuError> { | ||||||
|  |         self.map_err(|e| DekuError::Parse(e.to_string().into())) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[deku_derive(DekuRead)] | ||||||
|  | #[derive(Debug, PartialEq)] | ||||||
|  | pub(crate) struct DataField { | ||||||
|  |     #[deku(reader = "read_size(deku::reader)")] | ||||||
|  |     pub(crate) data_size: ByteSize, | ||||||
|  |  | ||||||
|  |     #[deku(bytes_read = "data_size.0")] | ||||||
|  |     pub(crate) data: Vec<u8>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[deku_derive(DekuRead)] | ||||||
|  | #[derive(Debug, PartialEq)] | ||||||
|  | pub(crate) struct BlockHeader { | ||||||
|  |     pub(crate) group_id: GroupId, | ||||||
|  |  | ||||||
|  |     #[deku(reader = "read_size(deku::reader)")] | ||||||
|  |     pub(crate) data_size: ByteSize, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[deku_derive(DekuRead)] | ||||||
|  | #[derive(Debug, PartialEq)] | ||||||
|  | pub(crate) struct DataBlock { | ||||||
|  |     pub(crate) header: BlockHeader, | ||||||
|  |  | ||||||
|  |     #[deku(ctx = "header.group_id")] | ||||||
|  |     pub(crate) inner: DataGroup, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn read_size<R: std::io::Read>(reader: &mut Reader<R>) -> Result<ByteSize, DekuError> { | ||||||
|  |     let first_byte: u8 = u8::from_reader_with_ctx(reader, ())?; | ||||||
|  |  | ||||||
|  |     let is_length_expanded = first_byte.get_bit::<Msb0>(BitIdx::new(0).map_to_deku_parse_error()?); | ||||||
|  |  | ||||||
|  |     match is_length_expanded { | ||||||
|  |         true => { | ||||||
|  |             let size_of_data_size: ByteSize = ByteSize((first_byte & 0b0111_1111) as usize); | ||||||
|  |  | ||||||
|  |             if size_of_data_size.0 > 4 { | ||||||
|  |                 return Err(DekuError::Parse("Size of the length encoding is > 4, this is not normal. Probable parsing error".to_string().into())); | ||||||
|  |             }; | ||||||
|  |  | ||||||
|  |             // maximum size of the buffer is 4, we use the offset to read values less than 4 bytes | ||||||
|  |             let buffer: &mut [u8; 4] = &mut [0; 4]; | ||||||
|  |             let write_offset = 4 - size_of_data_size.0; | ||||||
|  |  | ||||||
|  |             match reader.read_bytes(size_of_data_size.0, &mut buffer[write_offset..])? { | ||||||
|  |                 ReaderRet::Bits(_bit_vec) => Err(DekuError::Parse("Got bits when trying to read bytes -> reader is unaligned, this is not normal.".to_string().into())), | ||||||
|  |                 ReaderRet::Bytes => Ok(ByteSize(u32::from_be_bytes(*buffer) as usize)), | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         false => Ok(ByteSize(first_byte as usize)), | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Using this as the map function asks deku to parse a datafield | ||||||
|  | // We then use the datafield and convert it to the corresponding value | ||||||
|  | fn convert_from_data_field<T>(data_field: DataField) -> Result<T, DekuError> | ||||||
|  | where | ||||||
|  |     T: FromStr, | ||||||
|  |     T::Err: Error, | ||||||
|  | { | ||||||
|  |     let text = String::from_utf8(data_field.data).map_to_deku_parse_error()?; | ||||||
|  |     T::from_str(&text).map_to_deku_parse_error() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[deku_derive(DekuRead)] | ||||||
|  | #[derive(Debug, PartialEq)] | ||||||
|  | pub struct SSVVersionNumber(#[deku(map = "convert_from_data_field")] u16); | ||||||
|  |  | ||||||
|  | #[deku_derive(DekuRead)] | ||||||
|  | #[derive(Debug, PartialEq)] | ||||||
|  | pub struct GALSSVersionNumber(#[deku(map = "convert_from_data_field")] u16); | ||||||
|  |  | ||||||
|  | #[deku_derive(DekuRead)] | ||||||
|  | #[derive(Debug, PartialEq)] | ||||||
|  | pub struct PSSVersionNumber(#[deku(map = "convert_from_data_field")] u16); | ||||||
|  |  | ||||||
|  | #[deku_derive(DekuRead)] | ||||||
|  | #[derive(Debug, PartialEq)] | ||||||
|  | pub struct ConfigurationHeader { | ||||||
|  |     pub ssv_version: SSVVersionNumber, | ||||||
|  |     pub galss_version: GALSSVersionNumber, | ||||||
|  |     pub pss_version: PSSVersionNumber, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[deku_derive(DekuRead)] | ||||||
|  | #[derive(Debug, PartialEq)] | ||||||
|  | pub struct PCSCReaderName(#[deku(map = "convert_from_data_field")] String); | ||||||
|  |  | ||||||
|  | #[deku_derive(DekuRead)] | ||||||
|  | #[derive(Debug, PartialEq)] | ||||||
|  | pub struct CardType(#[deku(map = "convert_from_data_field")] u8); | ||||||
|  |  | ||||||
|  | #[deku_derive(DekuRead)] | ||||||
|  | #[derive(Debug, PartialEq)] | ||||||
|  | pub struct PCSCReader { | ||||||
|  |     pub name: PCSCReaderName, | ||||||
|  |     pub card_type: CardType, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[deku_derive(DekuRead)] | ||||||
|  | #[derive(Debug, PartialEq)] | ||||||
|  | pub struct SESAMVitaleComponentID(#[deku(map = "convert_from_data_field")] u16); | ||||||
|  |  | ||||||
|  | #[deku_derive(DekuRead)] | ||||||
|  | #[derive(Debug, PartialEq)] | ||||||
|  | pub struct SESAMVitaleComponentDescription(#[deku(map = "convert_from_data_field")] String); | ||||||
|  |  | ||||||
|  | #[deku_derive(DekuRead)] | ||||||
|  | #[derive(Debug, PartialEq)] | ||||||
|  | pub struct SESAMVitaleComponentVersion(#[deku(map = "convert_from_data_field")] String); | ||||||
|  |  | ||||||
|  | #[deku_derive(DekuRead)] | ||||||
|  | #[derive(Debug, PartialEq)] | ||||||
|  | pub struct SESAMVitaleComponent { | ||||||
|  |     pub id: SESAMVitaleComponentID, | ||||||
|  |     pub description: SESAMVitaleComponentDescription, | ||||||
|  |     pub version: SESAMVitaleComponentVersion, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[deku_derive(DekuRead)] | ||||||
|  | #[derive(Debug, PartialEq)] | ||||||
|  | pub struct ReaderConfiguration {} | ||||||
|  |  | ||||||
|  | #[deku_derive(DekuRead)] | ||||||
|  | #[derive(Debug, PartialEq)] | ||||||
|  | #[deku(ctx = "group_id: GroupId", id = "group_id.0")] | ||||||
|  | pub enum DataGroup { | ||||||
|  |     #[deku(id = 60)] | ||||||
|  |     ConfigurationHeader(ConfigurationHeader), | ||||||
|  |     #[deku(id = 61)] | ||||||
|  |     ReaderConfiguration(ReaderConfiguration), | ||||||
|  |     #[deku(id = 64)] | ||||||
|  |     SESAMVitaleComponent(SESAMVitaleComponent), | ||||||
|  |     #[deku(id = 67)] | ||||||
|  |     PCSCReader(PCSCReader), | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug)] | ||||||
|  | pub enum ConfigurationError { | ||||||
|  |     MultipleConfigurationHeaders, | ||||||
|  |     MissingConfigurationHeader, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl fmt::Display for ConfigurationError { | ||||||
|  |     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||||
|  |         match self { | ||||||
|  |             ConfigurationError::MultipleConfigurationHeaders => { | ||||||
|  |                 write!(f, "Multiple ConfigurationHeader blocks found") | ||||||
|  |             } | ||||||
|  |             ConfigurationError::MissingConfigurationHeader => { | ||||||
|  |                 write!(f, "Missing ConfigurationHeader block") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Error for ConfigurationError {} | ||||||
|  |  | ||||||
|  | #[derive(Debug)] | ||||||
|  | pub struct Configuration { | ||||||
|  |     pub configuration_header: ConfigurationHeader, | ||||||
|  |     pub reader_configurations: Vec<ReaderConfiguration>, | ||||||
|  |     pub sesam_vitale_components: Vec<SESAMVitaleComponent>, | ||||||
|  |     pub pcsc_readers: Vec<PCSCReader>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl TryFrom<Vec<DataBlock>> for Configuration { | ||||||
|  |     type Error = ConfigurationError; | ||||||
|  |  | ||||||
|  |     fn try_from(data_blocks: Vec<DataBlock>) -> Result<Self, Self::Error> { | ||||||
|  |         let mut configuration_header: Option<ConfigurationHeader> = None; | ||||||
|  |         let mut reader_configurations: Vec<ReaderConfiguration> = Vec::new(); | ||||||
|  |         let mut sesam_vitale_components: Vec<SESAMVitaleComponent> = Vec::new(); | ||||||
|  |         let mut pcsc_readers: Vec<PCSCReader> = Vec::new(); | ||||||
|  |  | ||||||
|  |         for block in data_blocks { | ||||||
|  |             match block.inner { | ||||||
|  |                 DataGroup::ConfigurationHeader(header) => { | ||||||
|  |                     if configuration_header.is_some() { | ||||||
|  |                         return Err(ConfigurationError::MultipleConfigurationHeaders); | ||||||
|  |                     } | ||||||
|  |                     configuration_header = Some(header); | ||||||
|  |                 } | ||||||
|  |                 DataGroup::ReaderConfiguration(configuration) => { | ||||||
|  |                     reader_configurations.push(configuration) | ||||||
|  |                 } | ||||||
|  |                 DataGroup::SESAMVitaleComponent(component) => { | ||||||
|  |                     sesam_vitale_components.push(component); | ||||||
|  |                 } | ||||||
|  |                 DataGroup::PCSCReader(reader) => { | ||||||
|  |                     pcsc_readers.push(reader); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         let configuration_header = match configuration_header { | ||||||
|  |             Some(header) => header, | ||||||
|  |             None => return Err(ConfigurationError::MissingConfigurationHeader), | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         Ok(Self { | ||||||
|  |             configuration_header, | ||||||
|  |             reader_configurations, | ||||||
|  |             sesam_vitale_components, | ||||||
|  |             pcsc_readers, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub(crate) fn read_from_buffer<T: TryFrom<Vec<DataBlock>>>(buffer: &[u8]) -> Result<T, T::Error>{ | ||||||
|  |     let mut data_blocks: Vec<DataBlock> = Vec::new(); | ||||||
|  |     let mut offset = 0; | ||||||
|  |      | ||||||
|  |     let mut remaining_buffer = buffer; | ||||||
|  |  | ||||||
|  |     while !remaining_buffer.is_empty() { | ||||||
|  |         // TODO: properly handle errors | ||||||
|  |         let (rest, data_block) = DataBlock::from_bytes((remaining_buffer, offset)).unwrap(); | ||||||
|  |  | ||||||
|  |         data_blocks.push(data_block); | ||||||
|  |  | ||||||
|  |         (remaining_buffer, offset) = rest; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     T::try_from(data_blocks) | ||||||
|  | } | ||||||
							
								
								
									
										45
									
								
								crates/services-sesam-vitale-sys/src/types/types.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								crates/services-sesam-vitale-sys/src/types/types.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | |||||||
|  | pub(crate) use crate::types::common::IdentificationNationale; | ||||||
|  |  | ||||||
|  | use super::common::{ | ||||||
|  |     Byte, CategorieCarteProfessionnelSante, CodeAgrement, CodeCivilite, CodeConventionnel, | ||||||
|  |     CodeSpecialite, CodeZoneIK, CodeZoneTarifaire, Identification, IdentificationFacturation, | ||||||
|  |     IdentificationStructure, ModeExercice, SecteurActivite, StatutExercice, | ||||||
|  |     TypeCarteProfessionnelSante, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | pub(crate) struct CarteProfessionnelSante { | ||||||
|  |     type_carte: TypeCarteProfessionnelSante, | ||||||
|  |     categorie_carte: CategorieCarteProfessionnelSante, | ||||||
|  |     professionnel_sante: ProfessionnelDeSante, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct ProfessionnelDeSante { | ||||||
|  |     prenom: String, | ||||||
|  |     nom: String, | ||||||
|  |     code_civilite: CodeCivilite, | ||||||
|  |     identification_nationale: Identification<IdentificationNationale>, | ||||||
|  |     situations_execice: Vec<SituationDExercice>, | ||||||
|  | } | ||||||
|  | struct StructureMedicale { | ||||||
|  |     /// Nom Entreprise | ||||||
|  |     raison_sociale: String,  | ||||||
|  |     identification: Identification<IdentificationStructure>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct SituationDExercice { | ||||||
|  |     /// Numéro identifiant la situation du PS parmi ses autres situations inscrites sur sa CPS | ||||||
|  |     identifiant_situation: Byte, | ||||||
|  |     mode_exercice: Option<ModeExercice>, | ||||||
|  |     statut_exercice: Option<StatutExercice>, | ||||||
|  |     secteur_activite: Option<SecteurActivite>, | ||||||
|  |     structure_d_exercice: Option<StructureMedicale>, | ||||||
|  |     identification_facturation: Identification<IdentificationFacturation>, | ||||||
|  |     identification_remplacant: Option<Identification<IdentificationNationale>>, | ||||||
|  |     code_conventionnel: CodeConventionnel, | ||||||
|  |     code_specialite: CodeSpecialite, | ||||||
|  |     code_zone_tarifaire: CodeZoneTarifaire, | ||||||
|  |     code_zone_ik: CodeZoneIK, | ||||||
|  |     code_agrement: CodeAgrement, | ||||||
|  |     habilite_signature_facture: bool, | ||||||
|  |     habilite_signature_lot: bool, | ||||||
|  | } | ||||||
| @@ -4,10 +4,8 @@ version = "0.1.0" | |||||||
| edition = "2021" | edition = "2021" | ||||||
|  |  | ||||||
| [dependencies] | [dependencies] | ||||||
| anyhow = "1.0" | dotenv = "0.15" | ||||||
| libc = "0.2" | libc = "0.2" | ||||||
| thiserror = "1.0" |  | ||||||
| utils = { path = "../utils" } |  | ||||||
|  |  | ||||||
| [build-dependencies] | [build-dependencies] | ||||||
| dotenv = "0.15" | dotenv = "0.15" | ||||||
|   | |||||||
| @@ -1,13 +1,13 @@ | |||||||
|  | extern crate dotenv; | ||||||
|  |  | ||||||
| use std::env; | use std::env; | ||||||
| use std::path::PathBuf; | use std::path::PathBuf; | ||||||
|  |  | ||||||
| use dotenv::from_path; |  | ||||||
|  |  | ||||||
| fn main() { | fn main() { | ||||||
|     // Load the .env.build file for build-time environment variables |     // Load the .env.build file for build-time environment variables | ||||||
|     let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set"); |     let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); | ||||||
|     let manifest_path = PathBuf::from(manifest_dir); |     let manifest_path = PathBuf::from(manifest_dir); | ||||||
|     from_path(manifest_path.join(".env.build")).ok(); |     dotenv::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_LIB_PATH"); | ||||||
|     println!("cargo::rerun-if-env-changed=SESAM_FSV_SSVLIB"); |     println!("cargo::rerun-if-env-changed=SESAM_FSV_SSVLIB"); | ||||||
| @@ -16,28 +16,21 @@ fn main() { | |||||||
|  |  | ||||||
|     // Add local lib directory to the linker search path (for def files and static libs) |     // Add local lib directory to the linker search path (for def files and static libs) | ||||||
|     let static_lib_path = manifest_path.join("lib"); |     let static_lib_path = manifest_path.join("lib"); | ||||||
|     println!( |     println!("cargo::rustc-link-search=native={}", static_lib_path.display()); | ||||||
|         "cargo::rustc-link-search=native={}", |  | ||||||
|         static_lib_path.display() |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     // Add the SESAM_FSV_LIB_PATH to the linker search path |     // Add the SESAM_FSV_LIB_PATH to the linker search path | ||||||
|     let fsv_lib_path = |     let fsv_lib_path = PathBuf::from(env::var("SESAM_FSV_LIB_PATH").unwrap()); | ||||||
|         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()); |     println!("cargo::rustc-link-search=native={}", fsv_lib_path.display()); | ||||||
|  |  | ||||||
|     // Add the SESAM_FSV_LIB_PATH to the PATH environment variable |     // Add the SESAM_FSV_LIB_PATH to the PATH environment variable | ||||||
|     if cfg!(target_os = "windows") { |     if cfg!(target_os = "windows") { | ||||||
|         let path = env::var("PATH").unwrap_or_default(); |         let path = env::var("PATH").unwrap_or(String::new()); | ||||||
|         println!("cargo:rustc-env=PATH={};{}", fsv_lib_path.display(), path); |         println!("cargo:rustc-env=PATH={};{}", fsv_lib_path.display(), path); | ||||||
|     } else if cfg!(target_os = "linux") { |     } else if cfg!(target_os = "linux") { | ||||||
|         println!("cargo:rustc-env=LD_LIBRARY_PATH={}", fsv_lib_path.display()); |         println!("cargo:rustc-env=LD_LIBRARY_PATH={}", fsv_lib_path.display()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Link the SESAM_FSV_SSVLIB dynamic library |     // Link the SESAM_FSV_SSVLIB dynamic library | ||||||
|     println!( |     println!("cargo::rustc-link-lib=dylib={}", env::var("SESAM_FSV_SSVLIB").unwrap()); | ||||||
|         "cargo::rustc-link-lib=dylib={}", |  | ||||||
|         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` |     // TODO : try `raw-dylib` instead of `dylib` on Windows to avoid the need of the `lib` headers compiled from the `def` | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,24 +1,9 @@ | |||||||
| use libc::{c_void, size_t}; | use libc::{c_void, size_t}; | ||||||
| use std::ffi::CString; | use std::ffi::CString; | ||||||
| use std::ptr; | use std::ptr; | ||||||
| use thiserror::Error; |  | ||||||
|  |  | ||||||
| use crate::libssv::{self, SSV_LireCartePS}; | use crate::libssv::SSV_LireCartePS; | ||||||
| use crate::ssv_memory::{decode_ssv_memory, Block, SSVMemoryError}; | use crate::ssv_memory::{decode_ssv_memory, Block}; | ||||||
|  |  | ||||||
| #[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)] | #[derive(Debug, Default)] | ||||||
| pub struct CartePS { | pub struct CartePS { | ||||||
| @@ -67,10 +52,10 @@ struct SituationPS { | |||||||
|     habilitation_à_signer_un_lot: String, |     habilitation_à_signer_un_lot: String, | ||||||
| } | } | ||||||
|  |  | ||||||
| pub fn lire_carte(code_pin: &str, lecteur: &str) -> Result<CartePS, CartePSError> { | pub fn lire_carte(code_pin: &str, lecteur: &str) -> Result<CartePS, String> { | ||||||
|     let resource_ps = CString::new(lecteur)?; |     let resource_ps = CString::new(lecteur).expect("CString::new failed"); | ||||||
|     let resource_reader = CString::new("")?; |     let resource_reader = CString::new("").expect("CString::new failed"); | ||||||
|     let card_number = CString::new(code_pin)?; |     let card_number = CString::new(code_pin).expect("CString::new failed"); | ||||||
|  |  | ||||||
|     let mut buffer: *mut c_void = ptr::null_mut(); |     let mut buffer: *mut c_void = ptr::null_mut(); | ||||||
|     let mut size: size_t = 0; |     let mut size: size_t = 0; | ||||||
| @@ -84,32 +69,17 @@ pub fn lire_carte(code_pin: &str, lecteur: &str) -> Result<CartePS, CartePSError | |||||||
|             &mut size, |             &mut size, | ||||||
|         ); |         ); | ||||||
|         println!("SSV_LireCartePS result: {}", result); |         println!("SSV_LireCartePS result: {}", result); | ||||||
|         if result != 0 { |  | ||||||
|             return Err(libssv::LibSSVError::StandardErrorCode { |  | ||||||
|                 code: result, |  | ||||||
|                 function: "SSV_LireCartePS", |  | ||||||
|             } |  | ||||||
|             .into()); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if !buffer.is_null() { |         if !buffer.is_null() { | ||||||
|             hex_values = std::slice::from_raw_parts(buffer as *const u8, size); |             hex_values = std::slice::from_raw_parts(buffer as *const u8, size); | ||||||
|             libc::free(buffer); |             libc::free(buffer); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     let groups = |     let groups = decode_ssv_memory(hex_values, hex_values.len()); | ||||||
|         decode_ssv_memory(hex_values, hex_values.len()).map_err(CartePSError::SSVMemory)?; |  | ||||||
|     decode_carte_ps(groups) |     decode_carte_ps(groups) | ||||||
| } | } | ||||||
|  |  | ||||||
| fn get_last_mut_situation(carte_ps: &mut CartePS) -> Result<&mut SituationPS, CartePSError> { | fn decode_carte_ps(groups: Vec<Block>) -> Result<CartePS, String> { | ||||||
|     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(); |     let mut carte_ps = CartePS::default(); | ||||||
|     for group in groups { |     for group in groups { | ||||||
|         for field in group.content { |         for field in group.content { | ||||||
| @@ -148,99 +118,137 @@ fn decode_carte_ps(groups: Vec<Block>) -> Result<CartePS, CartePSError> { | |||||||
|                 } |                 } | ||||||
|                 (2..=16, 1) => { |                 (2..=16, 1) => { | ||||||
|                     carte_ps.situations.push(SituationPS::default()); |                     carte_ps.situations.push(SituationPS::default()); | ||||||
|                     get_last_mut_situation(&mut carte_ps)? |                     carte_ps | ||||||
|  |                         .situations | ||||||
|  |                         .last_mut() | ||||||
|  |                         .unwrap() | ||||||
|                         .numero_logique_de_la_situation_de_facturation_du_ps = field.content[0]; |                         .numero_logique_de_la_situation_de_facturation_du_ps = field.content[0]; | ||||||
|                 } |                 } | ||||||
|                 (2..=16, 2) => { |                 (2..=16, 2) => { | ||||||
|                     get_last_mut_situation(&mut carte_ps)?.mode_d_exercice = |                     carte_ps.situations.last_mut().unwrap().mode_d_exercice = | ||||||
|                         String::from_utf8_lossy(field.content).to_string(); |                         String::from_utf8_lossy(field.content).to_string(); | ||||||
|                 } |                 } | ||||||
|                 (2..=16, 3) => { |                 (2..=16, 3) => { | ||||||
|                     get_last_mut_situation(&mut carte_ps)?.statut_d_exercice = |                     carte_ps.situations.last_mut().unwrap().statut_d_exercice = | ||||||
|                         String::from_utf8_lossy(field.content).to_string(); |                         String::from_utf8_lossy(field.content).to_string(); | ||||||
|                 } |                 } | ||||||
|                 (2..=16, 4) => { |                 (2..=16, 4) => { | ||||||
|                     get_last_mut_situation(&mut carte_ps)?.secteur_d_activite = |                     carte_ps.situations.last_mut().unwrap().secteur_d_activite = | ||||||
|                         String::from_utf8_lossy(field.content).to_string(); |                         String::from_utf8_lossy(field.content).to_string(); | ||||||
|                 } |                 } | ||||||
|                 (2..=16, 5) => { |                 (2..=16, 5) => { | ||||||
|                     get_last_mut_situation(&mut carte_ps)?.type_d_identification_structure = |                     carte_ps | ||||||
|  |                         .situations | ||||||
|  |                         .last_mut() | ||||||
|  |                         .unwrap() | ||||||
|  |                         .type_d_identification_structure = | ||||||
|                         String::from_utf8_lossy(field.content).to_string(); |                         String::from_utf8_lossy(field.content).to_string(); | ||||||
|                 } |                 } | ||||||
|                 (2..=16, 6) => { |                 (2..=16, 6) => { | ||||||
|                     get_last_mut_situation(&mut carte_ps)?.numero_d_identification_structure = |                     carte_ps | ||||||
|  |                         .situations | ||||||
|  |                         .last_mut() | ||||||
|  |                         .unwrap() | ||||||
|  |                         .numero_d_identification_structure = | ||||||
|                         String::from_utf8_lossy(field.content).to_string(); |                         String::from_utf8_lossy(field.content).to_string(); | ||||||
|                 } |                 } | ||||||
|                 (2..=16, 7) => { |                 (2..=16, 7) => { | ||||||
|                     get_last_mut_situation(&mut carte_ps)? |                     carte_ps | ||||||
|  |                         .situations | ||||||
|  |                         .last_mut() | ||||||
|  |                         .unwrap() | ||||||
|                         .cle_du_numero_d_identification_structure = |                         .cle_du_numero_d_identification_structure = | ||||||
|                         String::from_utf8_lossy(field.content).to_string(); |                         String::from_utf8_lossy(field.content).to_string(); | ||||||
|                 } |                 } | ||||||
|                 (2..=16, 8) => { |                 (2..=16, 8) => { | ||||||
|                     get_last_mut_situation(&mut carte_ps)?.raison_sociale_structure = |                     carte_ps | ||||||
|  |                         .situations | ||||||
|  |                         .last_mut() | ||||||
|  |                         .unwrap() | ||||||
|  |                         .raison_sociale_structure = | ||||||
|                         String::from_utf8_lossy(field.content).to_string(); |                         String::from_utf8_lossy(field.content).to_string(); | ||||||
|                 } |                 } | ||||||
|                 (2..=16, 9) => { |                 (2..=16, 9) => { | ||||||
|                     get_last_mut_situation(&mut carte_ps)? |                     carte_ps | ||||||
|  |                         .situations | ||||||
|  |                         .last_mut() | ||||||
|  |                         .unwrap() | ||||||
|                         .numero_d_identification_de_facturation_du_ps = |                         .numero_d_identification_de_facturation_du_ps = | ||||||
|                         String::from_utf8_lossy(field.content).to_string(); |                         String::from_utf8_lossy(field.content).to_string(); | ||||||
|                 } |                 } | ||||||
|                 (2..=16, 10) => { |                 (2..=16, 10) => { | ||||||
|                     get_last_mut_situation(&mut carte_ps)? |                     carte_ps | ||||||
|  |                         .situations | ||||||
|  |                         .last_mut() | ||||||
|  |                         .unwrap() | ||||||
|                         .cle_du_numero_d_identification_de_facturation_du_ps = |                         .cle_du_numero_d_identification_de_facturation_du_ps = | ||||||
|                         String::from_utf8_lossy(field.content).to_string(); |                         String::from_utf8_lossy(field.content).to_string(); | ||||||
|                 } |                 } | ||||||
|                 (2..=16, 11) => { |                 (2..=16, 11) => { | ||||||
|                     get_last_mut_situation(&mut carte_ps)? |                     carte_ps | ||||||
|  |                         .situations | ||||||
|  |                         .last_mut() | ||||||
|  |                         .unwrap() | ||||||
|                         .numero_d_identification_du_ps_remplaçant = |                         .numero_d_identification_du_ps_remplaçant = | ||||||
|                         String::from_utf8_lossy(field.content).to_string(); |                         String::from_utf8_lossy(field.content).to_string(); | ||||||
|                 } |                 } | ||||||
|                 (2..=16, 12) => { |                 (2..=16, 12) => { | ||||||
|                     get_last_mut_situation(&mut carte_ps)? |                     carte_ps | ||||||
|  |                         .situations | ||||||
|  |                         .last_mut() | ||||||
|  |                         .unwrap() | ||||||
|                         .cle_du_numero_d_identification_du_ps_remplaçant = |                         .cle_du_numero_d_identification_du_ps_remplaçant = | ||||||
|                         String::from_utf8_lossy(field.content).to_string(); |                         String::from_utf8_lossy(field.content).to_string(); | ||||||
|                 } |                 } | ||||||
|                 (2..=16, 13) => { |                 (2..=16, 13) => { | ||||||
|                     get_last_mut_situation(&mut carte_ps)?.code_conventionnel = |                     carte_ps.situations.last_mut().unwrap().code_conventionnel = | ||||||
|                         String::from_utf8_lossy(field.content).to_string(); |                         String::from_utf8_lossy(field.content).to_string(); | ||||||
|                 } |                 } | ||||||
|                 (2..=16, 14) => { |                 (2..=16, 14) => { | ||||||
|                     get_last_mut_situation(&mut carte_ps)?.code_specialite = |                     carte_ps.situations.last_mut().unwrap().code_specialite = | ||||||
|                         String::from_utf8_lossy(field.content).to_string(); |                         String::from_utf8_lossy(field.content).to_string(); | ||||||
|                 } |                 } | ||||||
|                 (2..=16, 15) => { |                 (2..=16, 15) => { | ||||||
|                     get_last_mut_situation(&mut carte_ps)?.code_zone_tarifaire = |                     carte_ps.situations.last_mut().unwrap().code_zone_tarifaire = | ||||||
|                         String::from_utf8_lossy(field.content).to_string(); |                         String::from_utf8_lossy(field.content).to_string(); | ||||||
|                 } |                 } | ||||||
|                 (2..=16, 16) => { |                 (2..=16, 16) => { | ||||||
|                     get_last_mut_situation(&mut carte_ps)?.code_zone_ik = |                     carte_ps.situations.last_mut().unwrap().code_zone_ik = | ||||||
|                         String::from_utf8_lossy(field.content).to_string(); |                         String::from_utf8_lossy(field.content).to_string(); | ||||||
|                 } |                 } | ||||||
|                 (2..=16, 17) => { |                 (2..=16, 17) => { | ||||||
|                     get_last_mut_situation(&mut carte_ps)?.code_agrement_1 = |                     carte_ps.situations.last_mut().unwrap().code_agrement_1 = | ||||||
|                         String::from_utf8_lossy(field.content).to_string(); |                         String::from_utf8_lossy(field.content).to_string(); | ||||||
|                 } |                 } | ||||||
|                 (2..=16, 18) => { |                 (2..=16, 18) => { | ||||||
|                     get_last_mut_situation(&mut carte_ps)?.code_agrement_2 = |                     carte_ps.situations.last_mut().unwrap().code_agrement_2 = | ||||||
|                         String::from_utf8_lossy(field.content).to_string(); |                         String::from_utf8_lossy(field.content).to_string(); | ||||||
|                 } |                 } | ||||||
|                 (2..=16, 19) => { |                 (2..=16, 19) => { | ||||||
|                     get_last_mut_situation(&mut carte_ps)?.code_agrement_3 = |                     carte_ps.situations.last_mut().unwrap().code_agrement_3 = | ||||||
|                         String::from_utf8_lossy(field.content).to_string(); |                         String::from_utf8_lossy(field.content).to_string(); | ||||||
|                 } |                 } | ||||||
|                 (2..=16, 20) => { |                 (2..=16, 20) => { | ||||||
|                     get_last_mut_situation(&mut carte_ps)?.habilitation_à_signer_une_facture = |                     carte_ps | ||||||
|  |                         .situations | ||||||
|  |                         .last_mut() | ||||||
|  |                         .unwrap() | ||||||
|  |                         .habilitation_à_signer_une_facture = | ||||||
|                         String::from_utf8_lossy(field.content).to_string(); |                         String::from_utf8_lossy(field.content).to_string(); | ||||||
|                 } |                 } | ||||||
|                 (2..=16, 21) => { |                 (2..=16, 21) => { | ||||||
|                     get_last_mut_situation(&mut carte_ps)?.habilitation_à_signer_un_lot = |                     carte_ps | ||||||
|  |                         .situations | ||||||
|  |                         .last_mut() | ||||||
|  |                         .unwrap() | ||||||
|  |                         .habilitation_à_signer_un_lot = | ||||||
|                         String::from_utf8_lossy(field.content).to_string(); |                         String::from_utf8_lossy(field.content).to_string(); | ||||||
|                 } |                 } | ||||||
|                 _ => { |                 _ => { | ||||||
|                     return Err(CartePSError::UnknownGroupFieldPair { |                     return Err(format!( | ||||||
|                         group: group.id, |                         "Unknown (group, field) pair: ({}, {})", | ||||||
|                         field: field.id, |                         group.id, field.id | ||||||
|                     }); |                     )) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| @@ -271,7 +279,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, |             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, |             48, 2, 48, 48, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49, | ||||||
|         ]; |         ]; | ||||||
|         let blocks = decode_ssv_memory(bytes, bytes.len()).unwrap(); |         let blocks = decode_ssv_memory(bytes, bytes.len()); | ||||||
|         let carte_ps = decode_carte_ps(blocks).unwrap(); |         let carte_ps = decode_carte_ps(blocks).unwrap(); | ||||||
|  |  | ||||||
|         assert_eq!(carte_ps.titulaire.type_de_carte_ps, "0"); |         assert_eq!(carte_ps.titulaire.type_de_carte_ps, "0"); | ||||||
| @@ -362,7 +370,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, |             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, |             48, 2, 48, 48, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49, | ||||||
|         ]; |         ]; | ||||||
|         let blocks = decode_ssv_memory(bytes, bytes.len()).unwrap(); |         let blocks = decode_ssv_memory(bytes, bytes.len()); | ||||||
|         let carte_ps = decode_carte_ps(blocks).unwrap(); |         let carte_ps = decode_carte_ps(blocks).unwrap(); | ||||||
|  |  | ||||||
|         assert_eq!(carte_ps.situations.len(), 3); |         assert_eq!(carte_ps.situations.len(), 3); | ||||||
|   | |||||||
| @@ -3,13 +3,6 @@ | |||||||
| /// Low level bindings to the SSVLIB dynamic library. | /// Low level bindings to the SSVLIB dynamic library. | ||||||
| // TODO : look for creating a dedicated *-sys crate : https://kornel.ski/rust-sys-crate | // 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 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 = "linux", link(name = "ssvlux64"))] | ||||||
| #[cfg_attr(target_os = "windows", link(name = "ssvw64"))] | #[cfg_attr(target_os = "windows", link(name = "ssvw64"))] | ||||||
|   | |||||||
| @@ -3,8 +3,6 @@ mod libssv; | |||||||
| mod ssv_memory; | mod ssv_memory; | ||||||
| mod ssvlib_demo; | mod ssvlib_demo; | ||||||
|  |  | ||||||
| use anyhow::{Context, Result}; | fn main() { | ||||||
|  |     ssvlib_demo::demo(); | ||||||
| fn main() -> Result<()> { |  | ||||||
|     ssvlib_demo::demo().context("Error while running the SSV library demo") |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,33 +1,6 @@ | |||||||
| /// # SSV Memory | /// # SSV Memory | ||||||
| /// Provide functions to manipulate raw memory from SSV library. | /// Provide functions to manipulate raw memory from SSV library. | ||||||
| use std::convert::TryFrom; | 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)] | #[derive(PartialEq, Debug)] | ||||||
| struct ElementSize { | struct ElementSize { | ||||||
| @@ -36,12 +9,13 @@ struct ElementSize { | |||||||
| } | } | ||||||
|  |  | ||||||
| // TODO : Est-ce qu'on pourrait/devrait définir un type custom pour représenter les tableaux de bytes ? | // TODO : Est-ce qu'on pourrait/devrait définir un type custom pour représenter les tableaux de bytes ? | ||||||
|  |  | ||||||
| impl TryFrom<&[u8]> for ElementSize { | impl TryFrom<&[u8]> for ElementSize { | ||||||
|     type Error = BytesReadingError; |     type Error = &'static str; | ||||||
|  |  | ||||||
|     fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> { |     fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> { | ||||||
|         if bytes.is_empty() { |         if bytes.is_empty() { | ||||||
|             return Err(BytesReadingError::EmptyBytes); |             return Err("Empty bytes input"); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         let mut element_size = ElementSize { size: 0, pad: 1 }; |         let mut element_size = ElementSize { size: 0, pad: 1 }; | ||||||
| @@ -56,15 +30,9 @@ impl TryFrom<&[u8]> for ElementSize { | |||||||
|             // N are the 7 lower bits of the first byte |             // N are the 7 lower bits of the first byte | ||||||
|             let size_bytes_len = (bytes[0] & 0b0111_1111) as usize; |             let size_bytes_len = (bytes[0] & 0b0111_1111) as usize; | ||||||
|             if size_bytes_len > bytes.len() - 1 { |             if size_bytes_len > bytes.len() - 1 { | ||||||
|                 return Err(BytesReadingError::InvalidSize { |                 return Err("Invalid memory: not enough bytes to read the size"); | ||||||
|                     expected: size_bytes_len, |  | ||||||
|                     actual: bytes.len() - 1, |  | ||||||
|                 }); |  | ||||||
|             } else if size_bytes_len > 4 { |             } else if size_bytes_len > 4 { | ||||||
|                 return Err(BytesReadingError::SizeTooBig { |                 return Err("Invalid memory: size is too big"); | ||||||
|                     expected: 4, |  | ||||||
|                     actual: size_bytes_len, |  | ||||||
|                 }); |  | ||||||
|             } |             } | ||||||
|             let size_bytes = &bytes[1..1 + size_bytes_len]; |             let size_bytes = &bytes[1..1 + size_bytes_len]; | ||||||
|  |  | ||||||
| @@ -86,21 +54,15 @@ pub struct Block<'a> { | |||||||
|     pub content: Vec<Field<'a>>, |     pub content: Vec<Field<'a>>, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl<'a> TryFrom<&'a [u8]> for Block<'a> { | impl<'a> From<&'a [u8]> for Block<'a> { | ||||||
|     type Error = BytesReadingError; |     fn from(bytes: &'a [u8]) -> Self { | ||||||
|  |  | ||||||
|     fn try_from(bytes: &'a [u8]) -> Result<Self, Self::Error> { |  | ||||||
|         let mut offset = 0; |         let mut offset = 0; | ||||||
|         let id = u16::from_be_bytes( |         let id = u16::from_be_bytes(bytes[..2].try_into().unwrap()); | ||||||
|             bytes[..2] |  | ||||||
|                 .try_into() |  | ||||||
|                 .map_err(BytesReadingError::InvalidBlockId)?, |  | ||||||
|         ); |  | ||||||
|         offset += 2; |         offset += 2; | ||||||
|         let ElementSize { |         let ElementSize { | ||||||
|             size: block_size, |             size: block_size, | ||||||
|             pad, |             pad, | ||||||
|         } = bytes[2..].try_into()?; |         } = bytes[2..].try_into().unwrap(); | ||||||
|         offset += pad; |         offset += pad; | ||||||
|         let raw_content = &bytes[offset..]; |         let raw_content = &bytes[offset..]; | ||||||
|         let mut field_offset = 0; |         let mut field_offset = 0; | ||||||
| @@ -108,22 +70,17 @@ impl<'a> TryFrom<&'a [u8]> for Block<'a> { | |||||||
|         let mut content = Vec::new(); |         let mut content = Vec::new(); | ||||||
|         let mut field_id = 1; |         let mut field_id = 1; | ||||||
|         while field_offset < block_size { |         while field_offset < block_size { | ||||||
|             let mut field: Field<'a> = raw_content[field_offset..].try_into().map_err(|err| { |             let mut field: Field<'a> = raw_content[field_offset..].into(); | ||||||
|                 BytesReadingError::InvalidField { |  | ||||||
|                     source: Box::new(err), |  | ||||||
|                     offset: field_offset, |  | ||||||
|                 } |  | ||||||
|             })?; |  | ||||||
|             field.id = field_id; |             field.id = field_id; | ||||||
|             field_offset += field.size; |             field_offset += field.size; | ||||||
|             field_id += 1; |             field_id += 1; | ||||||
|             content.push(field); |             content.push(field); | ||||||
|         } |         } | ||||||
|         Ok(Block { |         Block { | ||||||
|             id, |             id, | ||||||
|             size: offset + block_size, |             size: offset + block_size, | ||||||
|             content, |             content, | ||||||
|         }) |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -134,41 +91,31 @@ pub struct Field<'a> { | |||||||
|     pub content: &'a [u8], |     pub content: &'a [u8], | ||||||
| } | } | ||||||
|  |  | ||||||
| impl<'a> TryFrom<&'a [u8]> for Field<'a> { | impl<'a> From<&'a [u8]> for Field<'a> { | ||||||
|     type Error = BytesReadingError; |     fn from(bytes: &'a [u8]) -> Self { | ||||||
|  |         let ElementSize { size, pad } = bytes.try_into().unwrap(); | ||||||
|     fn try_from(bytes: &'a [u8]) -> Result<Self, Self::Error> { |  | ||||||
|         let ElementSize { size, pad } = bytes.try_into()?; |  | ||||||
|         let contenu = &bytes[pad..pad + size]; |         let contenu = &bytes[pad..pad + size]; | ||||||
|         Ok(Field { |         Field { | ||||||
|             id: 0, |             id: 0, | ||||||
|             size: pad + size, |             size: pad + size, | ||||||
|             content: contenu, |             content: contenu, | ||||||
|         }) |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| pub fn decode_ssv_memory(bytes: &[u8], size: usize) -> Result<Vec<Block>, SSVMemoryError> { | pub fn decode_ssv_memory(bytes: &[u8], size: usize) -> Vec<Block> { | ||||||
|     let mut blocks: Vec<Block> = Vec::new(); |     let mut blocks: Vec<Block> = Vec::new(); | ||||||
|     let mut offset = 0; |     let mut offset = 0; | ||||||
|     while offset < size { |     while offset < size { | ||||||
|         let block: Block = |         let block: Block = bytes[offset..].into(); | ||||||
|             bytes[offset..] |  | ||||||
|                 .try_into() |  | ||||||
|                 .map_err(|err| SSVMemoryError::BlockParsing { |  | ||||||
|                     source: err, |  | ||||||
|                     offset, |  | ||||||
|                 })?; |  | ||||||
|         offset += block.size; |         offset += block.size; | ||||||
|         blocks.push(block); |         blocks.push(block); | ||||||
|     } |     } | ||||||
|     Ok(blocks) |     blocks | ||||||
| } | } | ||||||
|  |  | ||||||
| #[cfg(test)] | #[cfg(test)] | ||||||
| mod test_element_size { | mod test_element_size { | ||||||
|     use std::any::Any; |  | ||||||
|  |  | ||||||
|     use super::*; |     use super::*; | ||||||
|  |  | ||||||
|     #[test] |     #[test] | ||||||
| @@ -195,51 +142,29 @@ mod test_element_size { | |||||||
|     #[test] |     #[test] | ||||||
|     fn null_size() { |     fn null_size() { | ||||||
|         let bytes: &[u8] = &[]; |         let bytes: &[u8] = &[]; | ||||||
|         let result: Result<ElementSize, BytesReadingError> = bytes.try_into(); |         let result: Result<ElementSize, &str> = bytes.try_into(); | ||||||
|         assert!(result.is_err()); |         assert_eq!(result, Err("Empty bytes input"),); | ||||||
|         assert_eq!( |  | ||||||
|             result.unwrap_err().type_id(), |  | ||||||
|             BytesReadingError::EmptyBytes.type_id() |  | ||||||
|         ); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     #[test] |     #[test] | ||||||
|     fn invalid_memory() { |     fn invalid_memory() { | ||||||
|         let bytes: &[u8] = &[0b_1000_0001_u8]; |         let bytes: &[u8] = &[0b_1000_0001_u8]; | ||||||
|         let result: Result<ElementSize, BytesReadingError> = bytes.try_into(); |         let result: Result<ElementSize, &str> = bytes.try_into(); | ||||||
|         assert!(result.is_err()); |  | ||||||
|         assert_eq!( |         assert_eq!( | ||||||
|             result.unwrap_err().to_string(), |             result, | ||||||
|             BytesReadingError::InvalidSize { |             Err("Invalid memory: not enough bytes to read the size"), | ||||||
|                 expected: 1, |  | ||||||
|                 actual: 0 |  | ||||||
|             } |  | ||||||
|             .to_string() |  | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         let bytes: &[u8] = &[0b_1000_0010_u8, 1]; |         let bytes: &[u8] = &[0b_1000_0010_u8, 1]; | ||||||
|         let result: Result<ElementSize, BytesReadingError> = bytes.try_into(); |         let result: Result<ElementSize, &str> = bytes.try_into(); | ||||||
|         assert!(result.is_err()); |  | ||||||
|         assert_eq!( |         assert_eq!( | ||||||
|             result.unwrap_err().to_string(), |             result, | ||||||
|             BytesReadingError::InvalidSize { |             Err("Invalid memory: not enough bytes to read the size"), | ||||||
|                 expected: 2, |  | ||||||
|                 actual: 1 |  | ||||||
|             } |  | ||||||
|             .to_string() |  | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         let bytes: &[u8] = &[0b_1000_0101_u8, 1, 1, 1, 1, 1]; |         let bytes: &[u8] = &[0b_1000_0101_u8, 1, 1, 1, 1, 1]; | ||||||
|         let result: Result<ElementSize, BytesReadingError> = bytes.try_into(); |         let result: Result<ElementSize, &str> = bytes.try_into(); | ||||||
|         assert!(result.is_err()); |         assert_eq!(result, Err("Invalid memory: size is too big"),); | ||||||
|         assert_eq!( |  | ||||||
|             result.unwrap_err().to_string(), |  | ||||||
|             BytesReadingError::SizeTooBig { |  | ||||||
|                 expected: 4, |  | ||||||
|                 actual: 5 |  | ||||||
|             } |  | ||||||
|             .to_string() |  | ||||||
|         ); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -254,7 +179,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, |             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, |             67, 79, 73, 83, 69, 1, 84, | ||||||
|         ]; |         ]; | ||||||
|         let element: Field = bytes.try_into().unwrap(); |         let element: Field = bytes.into(); | ||||||
|         assert_eq!(element.size, 52); |         assert_eq!(element.size, 52); | ||||||
|         assert_eq!(element.content[..5], [1, 48, 1, 56, 11]); |         assert_eq!(element.content[..5], [1, 48, 1, 56, 11]); | ||||||
|     } |     } | ||||||
| @@ -269,7 +194,7 @@ mod test_field { | |||||||
|         // Add 256 bytes to the content |         // Add 256 bytes to the content | ||||||
|         bytes_vec.append(&mut vec![1; 256]); |         bytes_vec.append(&mut vec![1; 256]); | ||||||
|         let bytes: &[u8] = &bytes_vec; |         let bytes: &[u8] = &bytes_vec; | ||||||
|         let element: Field = bytes.try_into().unwrap(); |         let element: Field = bytes.into(); | ||||||
|         assert_eq!(element.size, 259); |         assert_eq!(element.size, 259); | ||||||
|         assert_eq!(element.content.len(), 256); |         assert_eq!(element.content.len(), 256); | ||||||
|     } |     } | ||||||
| @@ -283,15 +208,15 @@ mod test_block { | |||||||
|     fn test_francoise_pharmacien0052419_partial_block_1() { |     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 bytes: &[u8] = &[1, 48, 1, 56, 11, 57, 57, 55, 48, 48, 53, 50, 52, 49, 57, 52]; | ||||||
|  |  | ||||||
|         let field1: Field = bytes.try_into().unwrap(); |         let field1: Field = bytes.into(); | ||||||
|         assert_eq!(field1.size, 2); |         assert_eq!(field1.size, 2); | ||||||
|         assert_eq!(field1.content, &[48]); |         assert_eq!(field1.content, &[48]); | ||||||
|  |  | ||||||
|         let field2: Field = bytes[field1.size..].try_into().unwrap(); |         let field2: Field = bytes[field1.size..].into(); | ||||||
|         assert_eq!(field2.size, 2); |         assert_eq!(field2.size, 2); | ||||||
|         assert_eq!(field2.content, &[56]); |         assert_eq!(field2.content, &[56]); | ||||||
|  |  | ||||||
|         let field3: Field = bytes[field1.size + field2.size..].try_into().unwrap(); |         let field3: Field = bytes[field1.size + field2.size..].into(); | ||||||
|         assert_eq!(field3.size, 12); |         assert_eq!(field3.size, 12); | ||||||
|         assert_eq!( |         assert_eq!( | ||||||
|             field3.content, |             field3.content, | ||||||
| @@ -318,12 +243,12 @@ mod test_block { | |||||||
|             48, 2, 49, 48, 2, 48, 48, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49, |             48, 2, 49, 48, 2, 48, 48, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49, | ||||||
|         ]; |         ]; | ||||||
|  |  | ||||||
|         let first_block: Block = bytes.try_into().unwrap(); |         let first_block: Block = bytes.into(); | ||||||
|         assert_eq!(first_block.id, 1); |         assert_eq!(first_block.id, 1); | ||||||
|         assert_eq!(first_block.size, 54); |         assert_eq!(first_block.size, 54); | ||||||
|         assert_eq!(first_block.content.len(), 8); |         assert_eq!(first_block.content.len(), 8); | ||||||
|  |  | ||||||
|         let second_block: Block = bytes[first_block.size..].try_into().unwrap(); |         let second_block: Block = bytes[first_block.size..].into(); | ||||||
|         assert_eq!(second_block.id, 2); |         assert_eq!(second_block.id, 2); | ||||||
|         assert_eq!(second_block.size, 86); |         assert_eq!(second_block.size, 86); | ||||||
|         assert_eq!(second_block.content.len(), 21); |         assert_eq!(second_block.content.len(), 21); | ||||||
| @@ -352,7 +277,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, |             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, |             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()).unwrap(); |         let blocks = decode_ssv_memory(bytes, bytes.len()); | ||||||
|         assert_eq!(blocks.len(), 2); |         assert_eq!(blocks.len(), 2); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,54 +1,30 @@ | |||||||
| /// High level API for the SSV library, | /// High level API for the SSV library, | ||||||
| /// based on the low level bindings in libssv.rs. | /// based on the low level bindings in libssv.rs. | ||||||
|  | extern crate dotenv; | ||||||
| use libc::{c_void, size_t}; | use libc::{c_void, size_t}; | ||||||
| use std::env; | use std::env; | ||||||
| use std::ffi::CString; | use std::ffi::CString; | ||||||
|  | use std::path::PathBuf; | ||||||
| use std::ptr; | use std::ptr; | ||||||
| use thiserror::Error; |  | ||||||
|  |  | ||||||
| use crate::cps::lire_carte; | use crate::cps::lire_carte; | ||||||
| use crate::libssv::{SSV_InitLIB2, SSV_LireConfig}; | use crate::libssv::{SSV_InitLIB2, SSV_LireConfig}; | ||||||
|  |  | ||||||
| use ::utils::config::load_config; | fn ssv_init_lib_2() { | ||||||
|  |  | ||||||
| #[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_str = env::var("SESAM_INI_PATH").expect("SESAM_INI_PATH must be set"); | ||||||
|     let ini = CString::new(ini_str).expect("CString::new failed"); |     let ini = CString::new(ini_str).expect("CString::new failed"); | ||||||
|     unsafe { |     unsafe { | ||||||
|         let result = SSV_InitLIB2(ini.as_ptr()); |         let result = SSV_InitLIB2(ini.as_ptr()); | ||||||
|         println!("SSV_InitLIB2 result: {}", result); |         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() -> Result<(), SSVDemoError> { | fn ssv_lire_config() { | ||||||
|     let mut buffer: *mut c_void = ptr::null_mut(); |     let mut buffer: *mut c_void = ptr::null_mut(); | ||||||
|     let mut size: size_t = 0; |     let mut size: size_t = 0; | ||||||
|     unsafe { |     unsafe { | ||||||
|         let result = SSV_LireConfig(&mut buffer, &mut size); |         let result = SSV_LireConfig(&mut buffer, &mut size); | ||||||
|         println!("SSV_LireConfig result: {}", result); |         println!("SSV_LireConfig result: {}", result); | ||||||
|         if result != 0 { |  | ||||||
|             return Err(crate::libssv::LibSSVError::StandardErrorCode { |  | ||||||
|                 code: result, |  | ||||||
|                 function: "SSV_LireConfig", |  | ||||||
|             } |  | ||||||
|             .into()); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if !buffer.is_null() { |         if !buffer.is_null() { | ||||||
|             let hex_values = std::slice::from_raw_parts(buffer as *const u8, size); |             let hex_values = std::slice::from_raw_parts(buffer as *const u8, size); | ||||||
| @@ -60,26 +36,25 @@ fn ssv_lire_config() -> Result<(), SSVDemoError> { | |||||||
|             libc::free(buffer); |             libc::free(buffer); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     Ok(()) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| pub fn demo() -> Result<(), SSVDemoError> { | pub fn demo() { | ||||||
|     // 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` |     // 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 |     // 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 --------"); |     println!("------- Demo for the SSV library --------"); | ||||||
|  |  | ||||||
|     load_config()?; |     ssv_init_lib_2(); | ||||||
|  |  | ||||||
|     ssv_init_lib_2()?; |  | ||||||
|  |  | ||||||
|     let code_pin = "1234"; |     let code_pin = "1234"; | ||||||
|     let lecteur = "HID Global OMNIKEY 3x21 Smart Card Reader 0"; |     let lecteur = "HID Global OMNIKEY 3x21 Smart Card Reader 0"; | ||||||
|     let carte_ps = lire_carte(code_pin, lecteur)?; |     let carte_ps = lire_carte(code_pin, lecteur).unwrap(); | ||||||
|     println!("CartePS: {:#?}", carte_ps); |     println!("CartePS: {:#?}", carte_ps); | ||||||
|  |  | ||||||
|     ssv_lire_config()?; |     ssv_lire_config(); | ||||||
|  |  | ||||||
|     println!("-----------------------------------------"); |     println!("-----------------------------------------"); | ||||||
|     Ok(()) |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,9 +0,0 @@ | |||||||
| [package] |  | ||||||
| name = "utils" |  | ||||||
| version = "0.1.0" |  | ||||||
| edition = "2021" |  | ||||||
|  |  | ||||||
| [dependencies] |  | ||||||
| anyhow = "1.0" |  | ||||||
| directories = "5.0" |  | ||||||
| dotenv = "0.15" |  | ||||||
| @@ -1,48 +0,0 @@ | |||||||
| 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 +0,0 @@ | |||||||
| pub mod config; |  | ||||||
| @@ -1,85 +0,0 @@ | |||||||
| # 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) |  | ||||||
		Reference in New Issue
	
	Block a user