ryan/
lib.rs

1// #![warn(missing_docs)] // this can be annoying sometimes.
2#![forbid(unsafe_code)]
3
4//! # Ryan: a configuration language for the practical programmer
5//!
6//! Ryan is a minimal programming language that produces JSON (and therefore YAML) as
7//! output. It has builtin support for variables, imports and function calls while keeping
8//! things simple. The focus of these added features is to reduce code reuse when
9//! maintaining a sizable codebase of configuration files. It can also be used as an
10//! alternative to creating an overly complex CLI interfaces. Unsure on whether a value
11//! should be stored in a file or in an environment variable? Why not declare a huge
12//! configuration file with everything in it? You leave the users to decide where the
13//! values are coming from, giving them a versatile interface while keeping things simple
14//! on your side. Ryan makes that bridge while keeping the user's code short and
15//! maintainable.
16//!
17//! ## How to use Ryan in your program with this crate
18//!
19//! If you are not big on the fine details of Ryan or creating your own extensions, you
20//! can just use the function [`from_path`], which will give you the final Rust object you
21//! want from a file you specify. Thanks to [`serde`] and [`serde_json`], this function
22//! can be your one-stop-shop for everything Ryan related.
23//!
24//! However, if you are looking for ways to customize Ryan, the module [`environment::loader`]
25//! has the [`environment::ImportLoader`] trait (along with utilities) to configure the
26//! import mechanism however you like. On the other hand, the module [`environment::native`]
27//! has the interfaces for native extensions. Finally, everything can be put together
28//! in an environment using the [`environment::EnvironmentBuilder`].
29//!
30//! ## Ryan key principles
31//!
32//! It might look at first that adding one more thingamajig to your project might be
33//! overly complicated or even (God forbid!) dangerous. However, Ryan was created with
34//! your main concerns in mind and is _purposefully_ limited in scope. Here is how you
35//! **cannot** code a fully functional Pacman game in Ryan:
36//!
37//! 1. **(Configurable) hermeticity**: there is no `print` statement or any other kind
38//! side-effect to the language itself. The import system is the only way data can get
39//! into Ryan and even that can be easily disabled. Even if Ryan is not completely
40//! hermetic out-of-the-box, it can be made so in a couple of extra lines.
41//! 2. **Turing incompleteness**: this has to do mainly with loops. There is no `while`
42//! statement and you cannot recurse in Ryan. While you can iterate through data, you
43//! can do so only in pre-approved ways. This is done in such a way that every Ryan
44//! program is guaranteed to finish executing (eventually).
45//! 3. **Immutability**: everything in Ryan is immutable. Once a value is declared, it
46//! stays that way for the remaining of its existence. Of course, you can _shadow_ a
47//! variable by re-declaring it with another value, but that will be a completely new
48//! variable.
49//!
50//! Of course, one can reconfigure the import system to read from any arbitrary source of
51//! information and can also create _native extensions_ to throw all these guarantees out
52//! of the window. The possibilities are infinite. However, these are the sane defaults
53//! that are offered out-of-the-box.
54//!
55//! # A primer on Ryan
56//!
57//! In the first place, Ryan, just like YAML, is a superset of JSON. Therefore, every
58//! valid JSON is also valid Ryan:
59//! ```ryan
60//! {
61//!     "this": "works",
62//!     "that": ["is", "a", "list"],
63//!     "how_many_lights": 4,
64//!     "billion_dollar_mistake": null
65//! }
66//! ```
67//! However, JSON lacks many of the amenities we have grown so accustomed to:
68//! ```ryan
69//! // Comments...
70//! {
71//!     // Omitting annoying quotes:
72//!     this: "works",
73//!     // Forgiving commas:
74//!     that: ["is", "a", "list",],
75//!     // Basic maths
76//!     how_many_lights: 5 - 1,
77//!     billion_dollar_mistake: null,
78//! }
79//! ```
80//! Besides, since we are all about code reusability, defining variables is supported:
81//! ```ryan
82//! let lights = 4;
83//! {
84//!     "picard": lights,
85//!     "gul_madred": lights + 1,
86//! }
87//! ```
88//! But that is not all! Ryan is a _pattern matching_ language. Everything can be
89//! destructured down to its most basic components:
90//! ```ryan
91//! let { legends: { tanagra, temba, shaka }, .. } = {
92//!     participants: ["Picard", "Dathon"],
93//!     legends: {
94//!         tanagra: "Darmok and Jalad",
95//!         temba: "his arms wide",
96//!         shaka: "when the walls fell",
97//!     },
98//! };
99//!
100//! "Temba, " + temba    // "Temba, his arms wide"
101//! ```
102//! And last, but not least, you can import stuff, in a variety of ways:
103//! ```ryan
104//! // Will evaluate the file and import the final value into the variable:
105//! let q_episode_list = import "qEpisodes.ryan";
106//!
107//! // Will import "captain's log" as text, verbatim:
108//! let captains_log = import "captainsLog.txt" as text;
109//!
110//! // Will import value as text from an environment variable:
111//! let ensign_name = import "env:ENSIGN" as text;
112//!
113//! // Will import value as text or provide a default if not set:
114//! let cadet_name = import "env:CADET" as text or "Wesley Crusher";
115//!
116//! // No! No funny imports. Import string must be constant:
117//! let a_letter = "Q";
118//! let alien_entity = import "env:" + a_letter;    // <= parse error!
119//! ```
120//! Of course, there is some more to Ryan that this quick tour, but you already get the
121//! idea of the key components. To get the full picture, please refer to the book
122//! (under construction).
123//!
124
125/// Deserializes a Ryan value into a Rust struct using `serde`'s data model.
126mod de;
127/// The interface between Ryan and the rest of the world. Contains the import system and
128/// the native extension system.
129pub mod environment;
130/// The Ryan language _per se_, with parsing and evaluating functions and the types
131/// building the Abstract Syntax Tree.
132pub mod parser;
133/// The way Ryan allocates strings in memory.
134mod rc_world;
135/// Utilities for this crate.
136mod utils;
137
138pub use crate::de::DecodeError;
139pub use crate::environment::Environment;
140
141use serde::Deserialize;
142use std::{io::Read, path::Path};
143use thiserror::Error;
144
145use crate::parser::{EvalError, ParseError};
146
147/// The errors that may happen while processing Ryan programs.
148#[derive(Debug, Error)]
149pub enum Error {
150    /// An IO error happened (e.g., the file does not exist).
151    #[error("Io error: {0}")]
152    Io(std::io::Error),
153    /// A parse error happened.
154    #[error("{0}")]
155    Parse(ParseError),
156    /// A runtime error happened (e.g, there was a variable missing somewhere).
157    #[error("{0}")]
158    Eval(EvalError),
159    /// An error happened when transforming the final result to JSON.
160    #[error("Decode error: {0}")]
161    DecodeError(DecodeError),
162}
163
164/// Loads a Ryan file from disk and executes it, finally building an instance of type `T`
165/// from the execution outcome.
166pub fn from_path<P: AsRef<Path>, T>(path: P) -> Result<T, Error>
167where
168    T: for<'a> Deserialize<'a>,
169{
170    let file = std::fs::File::open(path.as_ref()).map_err(Error::Io)?;
171    let decoded = from_reader_with_filename(&path.as_ref().display().to_string(), file)?;
172
173    Ok(decoded)
174}
175
176/// Loads a Ryan file from disk and executes it, finally building an instance of type `T`
177/// from the execution outcome. This function takes an [`Environment`] as a parameter,
178/// that lets you have fine-grained control over imports and built-in functions.
179pub fn from_path_with_env<P: AsRef<Path>, T>(env: &Environment, path: P) -> Result<T, Error>
180where
181    T: for<'a> Deserialize<'a>,
182{
183    let mut patched_env = env.clone();
184    patched_env.current_module = Some(path.as_ref().display().to_string().into());
185    let file = std::fs::File::open(path.as_ref()).map_err(Error::Io)?;
186    let decoded = from_reader_with_env(&patched_env, file)?;
187
188    Ok(decoded)
189}
190
191/// Loads a Ryan file from a supplied reader and executes it, finally building an instance
192/// of type `T` from the execution outcome. The `current_module` will be set to `None`
193/// while executing in this mode.
194pub fn from_reader<R: Read, T>(mut reader: R) -> Result<T, Error>
195where
196    T: for<'a> Deserialize<'a>,
197{
198    let mut string = String::new();
199    reader.read_to_string(&mut string).map_err(Error::Io)?;
200    let decoded = from_str(&string)?;
201
202    Ok(decoded)
203}
204
205/// Loads a Ryan file from a supplied reader and executes it, finally building an instance
206/// of type `T` from the execution outcome. The `current_module` will be set to `name`
207/// while executing in this mode.
208pub fn from_reader_with_filename<R: Read, T>(name: &str, mut reader: R) -> Result<T, Error>
209where
210    T: for<'a> Deserialize<'a>,
211{
212    let mut string = String::new();
213    reader.read_to_string(&mut string).map_err(Error::Io)?;
214    let decoded = from_str_with_filename(name, &string)?;
215
216    Ok(decoded)
217}
218
219/// Loads a Ryan file from a supplied reader and executes it, finally building an instance
220/// of type `T`. from the execution outcome. This function takes an [`Environment`] as a
221/// parameter, that lets you have fine-grained control over imports, built-in functions and
222/// the `current_module` name.
223pub fn from_reader_with_env<R: Read, T>(env: &Environment, mut reader: R) -> Result<T, Error>
224where
225    T: for<'a> Deserialize<'a>,
226{
227    let mut string = String::new();
228    reader.read_to_string(&mut string).map_err(Error::Io)?;
229    let decoded = from_str_with_env(env, &string)?;
230
231    Ok(decoded)
232}
233
234/// Loads a Ryan file from a supplied string and executes it, finally building an instance
235/// of type `T` from the execution outcome. The `current_module` will be set to `None`
236/// while executing in this mode.
237pub fn from_str<T>(s: &str) -> Result<T, Error>
238where
239    T: for<'a> Deserialize<'a>,
240{
241    let env = Environment::new(None);
242    let parsed = parser::parse(&s).map_err(Error::Parse)?;
243    let value = parser::eval(env, &parsed).map_err(Error::Eval)?;
244    let decoded = value.decode::<T>().map_err(Error::DecodeError)?;
245
246    Ok(decoded)
247}
248
249/// Loads a Ryan file from a supplied string and executes it, finally building an instance
250/// of type `T` from the execution outcome. The `current_module` will be set to `name`
251/// while executing in this mode.
252pub fn from_str_with_filename<T>(name: &str, s: &str) -> Result<T, Error>
253where
254    T: for<'a> Deserialize<'a>,
255{
256    let env = Environment::new(Some(name));
257    let parsed = parser::parse(&s).map_err(Error::Parse)?;
258    let value = parser::eval(env, &parsed).map_err(Error::Eval)?;
259    let decoded = value.decode().map_err(Error::DecodeError)?;
260
261    Ok(decoded)
262}
263
264/// Loads a Ryan file from a supplied string and executes it, finally building an instance
265/// of type `T`. from the execution outcome. This function takes an [`Environment`] as a
266/// parameter, that lets you have fine-grained control over imports, built-in functions and
267/// the `current_module` name.
268pub fn from_str_with_env<T>(env: &Environment, s: &str) -> Result<T, Error>
269where
270    T: for<'a> Deserialize<'a>,
271{
272    let parsed = parser::parse(&s).map_err(Error::Parse)?;
273    let value = parser::eval(env.clone(), &parsed).map_err(Error::Eval)?;
274    let decoded = value.decode().map_err(Error::DecodeError)?;
275
276    Ok(decoded)
277}