outram_foam_basic_lib/io/mod.rs
1// SPDX-License-Identifier: GPL-3.0-only
2// Copyright (C) 2026 OUTRAM PARK contributors
3// Derived from OpenFOAM (www.openfoam.com)
4// Copyright (C) 2004-2023 OpenFOAM Foundation
5// Copyright (C) 2016-2023 OpenCFD Ltd.
6//
7// This file is part of OUTRAM PARK.
8//
9// OUTRAM PARK is free software: you can redistribute it and/or modify it
10// under the terms of the GNU General Public License as published by the
11// Free Software Foundation, either version 3 of the License, or (at your
12// option) any later version.
13//
14// OUTRAM PARK is distributed in the hope that it will be useful, but
15// WITHOUT ANY WARRANTY; without even the implied warranty of
16// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17// General Public License for more details.
18//
19// You should have received a copy of the GNU General Public License along
20// with OUTRAM PARK. If not, see <https://www.gnu.org/licenses/>.
21
22//! OpenFOAM ASCII **case I/O** — read and write OpenFOAM cases the way the
23//! upstream utilities do.
24//!
25//! This module is the foundation the OUTRAM PARK CLI reads/writes OpenFOAM
26//! cases with. The format and algorithms are OpenFOAM-derived (the `FoamFile`
27//! dictionary grammar, the `polyMesh` list layout, the time-directory field
28//! layout); this is an independent Rust re-implementation of the ASCII reader
29//! and writer, not the official OpenFOAM software.
30//!
31//! ## What lives here
32//!
33//! - [`dict`](crate::io::dict) — the `FoamFile` **dictionary** format: a tokeniser (strips
34//! `//` and `/* */` comments; treats `( ) { } ; [ ]` as delimiters), an
35//! in-memory AST ([`FoamDict`](crate::io::dict::FoamDict),
36//! [`FoamEntry`](crate::io::dict::FoamEntry),
37//! [`FoamValue`](crate::io::dict::FoamValue),
38//! [`Dimensioned`](crate::io::dict::Dimensioned)), the
39//! [`FoamHeader`](crate::io::dict::FoamHeader) block, and an exact-round-trip
40//! writer. Handles `system/controlDict`, `fvSchemes`, `fvSolution`-style
41//! dictionaries.
42//! - [`poly_mesh`](crate::io::poly_mesh) — [`PolyMesh`](crate::io::poly_mesh::PolyMesh): read/write `constant/polyMesh/{points,
43//! faces, owner, neighbour, boundary}` and convert to the crate's
44//! geometry-carrying [`crate::mesh::FvMesh`] via
45//! [`PolyMesh::to_fv_mesh`](crate::io::poly_mesh::PolyMesh::to_fv_mesh).
46//! - [`field`](crate::io::field) — read/write a time-directory field file
47//! (`0/p` volScalarField, `0/U` volVectorField): the `dimensions`,
48//! `internalField`, and `boundaryField` blocks ↔ the crate's
49//! [`crate::fields::VolScalarField`] / [`crate::fields::VolVectorField`].
50//! - [`case`](crate::io::case) — [`FoamCase`](crate::io::case::FoamCase): read a whole case directory (`system/…`,
51//! `constant/polyMesh`, `0/…`) into memory, with a best-effort writer.
52//!
53//! ## Round-trip guarantee
54//!
55//! The writer and parser are inverse at the **AST level**: constructing a
56//! [`FoamDict`](crate::io::dict::FoamDict) /
57//! [`PolyMesh`](crate::io::poly_mesh::PolyMesh) / field, serialising it, and parsing it back
58//! reproduces an equal in-memory value. (It is not a byte-for-byte re-emitter
59//! of an arbitrary pre-existing file — comment banners and incidental
60//! whitespace are normalised — but every value round-trips.)
61
62pub mod case;
63pub mod dict;
64pub mod field;
65pub mod poly_mesh;
66
67pub use case::FoamCase;
68pub use dict::{Dimensioned, FoamDict, FoamEntry, FoamHeader, FoamValue};
69pub use field::{
70 read_vol_scalar_field, read_vol_vector_field, write_vol_scalar_field, write_vol_vector_field,
71};
72pub use poly_mesh::{MeshFace, PolyMesh};
73
74/// Errors raised while reading or writing OpenFOAM ASCII case files.
75#[derive(Debug, thiserror::Error)]
76pub enum IoError {
77 /// An underlying filesystem error (file missing, permission denied, …).
78 #[error("I/O error on {path}: {source}")]
79 Io {
80 /// The path being read or written when the error occurred.
81 path: String,
82 /// The underlying `std::io` error.
83 source: std::io::Error,
84 },
85
86 /// The token stream did not match the expected grammar.
87 #[error("parse error in {context}: {message}")]
88 Parse {
89 /// What was being parsed (file name / entry / block).
90 context: String,
91 /// Human-readable description of the mismatch.
92 message: String,
93 },
94
95 /// A boundary-condition or field type that this reader does not yet
96 /// support was encountered.
97 #[error("unsupported {kind} `{name}` in {context}")]
98 Unsupported {
99 /// Category of the unsupported item (e.g. `"boundaryField type"`).
100 kind: String,
101 /// The offending type/keyword.
102 name: String,
103 /// Where it was found.
104 context: String,
105 },
106
107 /// The parsed topology could not be assembled into a valid mesh.
108 #[error("mesh construction error: {0}")]
109 Mesh(String),
110}
111
112impl IoError {
113 /// Build a [`IoError::Parse`] from a context label and message.
114 pub(crate) fn parse(context: impl Into<String>, message: impl Into<String>) -> Self {
115 Self::Parse {
116 context: context.into(),
117 message: message.into(),
118 }
119 }
120
121 /// Build a [`IoError::Io`] wrapping a filesystem error at `path`.
122 pub(crate) fn io(path: impl Into<String>, source: std::io::Error) -> Self {
123 Self::Io {
124 path: path.into(),
125 source,
126 }
127 }
128}