1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
//! Errors from running `inkling`.

use std::{error::Error, fmt};

use crate::{
    follow::ChoiceInfo,
    knot::{Address, AddressKind},
    line::Variable,
    node::Stack,
    story::Choice,
};

use std::cmp::Ordering;

#[derive(Clone, Debug)]
/// Errors from running a story.
///
/// This struct mostly concerns errors which will be encountered due to some mistake
/// with the story or user input.
///
/// `OutOfChoices` and `OutOfContent` are runtime errors from the story running out
/// of content to display. This is likely due to the story returning to a single knot
/// or stitch multiple times, consuming all of its choices if no fallback choice has
/// been added. These issues should be taken into account when writing the story:
/// if content will be returned to it is important to keep track of how many times
/// this is allowed to happen, or have a fallback in place.
///
/// All internal errors are contained in the `Internal` variant. These concern everything
/// that went wrong due to some issue within `inkling` itself. If you encounter any,
/// please open an issue on Github.
pub enum InklingError {
    /// Internal errors caused by `inkling`.
    Internal(InternalError),
    /// Used a knot or stitch name that is not present in the story as an input variable.
    InvalidAddress {
        knot: String,
        stitch: Option<String>,
    },
    /// An invalid choice index was given to resume the story with.
    InvalidChoice {
        /// Choice input by the user to resume the story with.
        selection: usize,
        /// List of choices that were available for the selection
        presented_choices: Vec<Choice>,
    },
    /// Used a variable name that is not present in the story as an input variable.
    InvalidVariable { name: String },
    /// Tried to compare variables of incomparable types to each other.
    InvalidVariableComparison {
        from: Variable,
        to: Variable,
        comparison: Ordering,
    },
    /// Called `make_choice` when no choice had been requested.
    ///
    /// Likely directly at the start of a story or after a `move_to` call was made.
    MadeChoiceWithoutChoice,
    /// No choices or fallback choices were available in a story branch at the given address.
    OutOfChoices { address: Address },
    /// No content was available for the story to continue from.
    OutOfContent,
    /// Tried to print a variable that cannot be printed.
    PrintInvalidVariable { name: String, value: Variable },
    /// Tried to resume a story that has not been started.
    ResumeBeforeStart,
    /// Tried to `start` a story that is already in progress.
    StartOnStoryInProgress,
    /// Tried to assign a new type to a variable.
    VariableTypeChange { from: Variable, to: Variable },
}

#[derive(Clone, Debug)]
/// Internal errors from `inkling`.
///
/// These are errors which arise when the library produces objects, trees, text
/// or internal stacks that are inconsistent with each other or themselves.
///
/// If the library is well written these should not possibly occur; at least until
/// this point every part of the internals are fully deterministic. That obviously
/// goes for a lot of buggy code that has been written since forever, so nothing
/// unique there.
///
/// Either way, all those sorts of errors are encapsulated here. They should never
/// be caused by invalid user input or Ink files, those errors should be captured
/// by either the parent [`InklingError`][crate::error::InklingError]
/// or parsing [`ParseError`][crate::error::ParseError] error structures.
pub enum InternalError {
    /// The internal stack of knots is inconsistent or has not been set properly.
    BadKnotStack(StackError),
    /// Could not `Process` a line of text into its final form.
    CouldNotProcess(ProcessError),
    /// Selected branch index does not exist.
    IncorrectChoiceIndex {
        selection: usize,
        available_choices: Vec<ChoiceInfo>,
        stack_index: usize,
        stack: Stack,
    },
    /// Current stack is not properly representing the graph or has some indexing problems.
    IncorrectNodeStack(IncorrectNodeStackError),
    /// Tried to use a variable address as a location.
    UseOfVariableAsLocation { name: String },
    /// Tried to use an unvalidated address after the story was parsed.
    UseOfUnvalidatedAddress { address: Address },
}

impl Error for InklingError {}
impl Error for InternalError {}

/// Wrapper to implement From for variants when the variant is simply encapsulated
/// in the enum.
///
/// # Example
/// Running
/// ```
/// impl_from_error[
///     MyError,
///     [Variant, ErrorData]
/// ];
/// ```
/// is identical to running
/// ```
/// impl From<ErrorData> for MyError {
///     from(err: ErrorData) -> Self {
///         Self::Variant(err)
///     }
/// }
/// ```
/// The macro can also implement several variants at once:
/// ```
/// impl_from_error[
///     MyError,
///     [Variant1, ErrorData1],
///     [Variant2, ErrorData2]
/// ];
/// ```
macro_rules! impl_from_error {
    ($for_type:ident; $([$variant:ident, $from_type:ident]),+) => {
        $(
            impl From<$from_type> for $for_type {
                fn from(err: $from_type) -> Self {
                    $for_type::$variant(err)
                }
            }
        )*
    }
}

impl_from_error![
    InklingError;
    [Internal, InternalError]
];

impl_from_error![
    InternalError;
    [BadKnotStack, StackError],
    [CouldNotProcess, ProcessError],
    [IncorrectNodeStack, IncorrectNodeStackError]
];

impl From<StackError> for InklingError {
    fn from(err: StackError) -> Self {
        InklingError::Internal(InternalError::BadKnotStack(err))
    }
}

impl fmt::Display for InklingError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        use InklingError::*;

        match self {
            Internal(err) => write!(f, "INTERNAL ERROR: {}", err),
            InvalidAddress { knot, stitch } => match stitch {
                Some(stitch_name) => write!(
                    f,
                    "Invalid address: knot '{}' does not contain a stitch named '{}'",
                    knot, stitch_name
                ),
                None => write!(
                    f,
                    "Invalid address: story does not contain a knot name '{}'",
                    knot
                ),
            },
            InvalidChoice {
                selection,
                presented_choices,
            } => write!(
                f,
                "Invalid selection of choice: selection was {} but number of choices was {} \
                 (maximum selection index is {})",
                selection,
                presented_choices.len(),
                presented_choices.len() - 1
            ),
            InvalidVariable { name } => write!(
                f,
                "Invalid variable: no variable with  name '{}' exists in the story",
                name
            ),
            InvalidVariableComparison {
                from,
                to,
                comparison,
            } => {
                let operator = match comparison {
                    Ordering::Equal => "==",
                    Ordering::Less => ">",
                    Ordering::Greater => "<",
                };

                write!(
                    f,
                    "Cannot compare variable of type '{}' to '{}' using the '{op}' operator \
                     (comparison was: '{:?} {op} {:?}')",
                    from.variant_string(),
                    to.variant_string(),
                    from,
                    to,
                    op = operator
                )
            }
            MadeChoiceWithoutChoice => write!(
                f,
                "Tried to make a choice, but no choice is currently active. Call `resume` \
                 and assert that a branching choice is returned before calling this again."
            ),
            OutOfChoices {
                address: Address::Validated(AddressKind::Location { knot, stitch }),
            } => write!(
                f,
                "Story reached a branching choice with no available choices to present \
                 or default choices to fall back on (knot: {}, stitch: {})",
                knot, stitch
            ),
            OutOfChoices { address } => write!(
                f,
                "Internal error: Tried to use a non-validated or non-location `Address` ('{:?}') \
                 when following a story",
                address
            ),
            OutOfContent => write!(f, "Story ran out of content before an end was reached"),
            PrintInvalidVariable { name, value } => write!(
                f,
                "Cannot print variable '{}' which has value '{:?}': invalid type",
                name, value
            ),
            ResumeBeforeStart => write!(f, "Tried to resume a story that has not yet been started"),
            StartOnStoryInProgress => {
                write!(f, "Called `start` on a story that is already in progress")
            }
            VariableTypeChange { from, to } => write!(
                f,
                "Cannot assign a value of type '{}' to a variable of type '{}' \
                 (variables cannot change type)",
                to.variant_string(),
                from.variant_string()
            ),
        }
    }
}

impl fmt::Display for InternalError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        use IncorrectNodeStackError::*;
        use InternalError::*;
        use ProcessErrorKind::*;
        use StackError::*;

        match self {
            BadKnotStack(err) => match err {
                BadAddress {
                    address: Address::Validated(AddressKind::Location { knot, stitch }),
                } => write!(
                    f,
                    "The currently set knot address (knot: {}, stitch: {}) does not \
                     actually represent a knot in the story",
                    knot, stitch
                ),
                BadAddress { address } => write!(
                    f,
                    "Tried to used a non-validated or non-location `Address` ('{:?}') in \
                     a function",
                    address
                ),
                NoLastChoices => write!(
                    f,
                    "Tried to follow with a choice but the last set of presented choices has \
                     not been saved"
                ),
                NoRootKnot { knot_name } => write!(
                    f,
                    "After reading a set of knots, the root knot with name {} \
                     does not exist in the set",
                    knot_name
                ),
                NoStack => write!(
                    f,
                    "There is no currently set knot or address to follow the story from"
                ),
            },
            CouldNotProcess(ProcessError { kind }) => match kind {
                InvalidAlternativeIndex => write!(
                    f,
                    "When processing an alternative, an invalid index was used to pick an item"
                ),
                InklingError(err) => write!(f, "{}", err),
            },
            IncorrectChoiceIndex {
                selection,
                ref available_choices,
                stack_index,
                ref stack,
            } => write!(
                f,
                "Tried to resume after a choice was made but the chosen index does not exist \
                 in the set of choices. Somehow a faulty set of choices was created from this \
                 branch point and returned upwards, the stack is wrong, or the wrong set of \
                 choices was used elsewhere in the preparation of the choice list. \
                 Selection index: {}, number of branches: {} \
                 (node level: {}, stack: {:?})",
                selection,
                available_choices.len(),
                stack_index,
                stack
            ),
            IncorrectNodeStack(err) => match err {
                EmptyStack => write!(f, "Tried to advance through a knot with an empty stack"),
                ExpectedBranchingPoint { stack_index, stack } => {
                    let item_number = stack[*stack_index];

                    write!(
                        f,
                        "While resuming a follow the stack found a regular line where \
                         it expected a branch point to nest deeper into. \
                         The stack has been corrupted. \
                         (stack level: {}, item number: {}, stack: {:?}",
                        stack_index, item_number, stack
                    )
                }
                MissingBranchIndex { stack_index, stack } => write!(
                    f,
                    "While resuming a follow the stack did not contain an index to \
                         select a branch with from a set of choices. The stack has been \
                         corrupted.
                         (stack level: {}, attempted index: {}, stack: {:?}",
                    stack_index,
                    stack_index + 1,
                    stack
                ),
                OutOfBounds {
                    stack_index,
                    stack,
                    num_items,
                } => write!(
                    f,
                    "Current stack has invalid index {} at node level {}: size of set is {} \
                     (stack: {:?})",
                    stack[*stack_index], stack_index, num_items, stack
                ),
            },
            UseOfVariableAsLocation { name } => write!(
                f,
                "Tried to use variable '{}' as a location in the story",
                name
            ),
            UseOfUnvalidatedAddress { address } => {
                write!(f, "Tried to use unvalidated address '{:?}'", address)
            }
        }
    }
}

#[derive(Clone, Debug)]
/// Error from processing content into its final format.
pub struct ProcessError {
    /// Error variant.
    pub kind: ProcessErrorKind,
}

impl From<InklingError> for ProcessError {
    fn from(err: InklingError) -> Self {
        ProcessError {
            kind: ProcessErrorKind::InklingError(Box::new(err)),
        }
    }
}

#[derive(Clone, Debug)]
/// Variant of `ProcessError`.
pub enum ProcessErrorKind {
    /// An `Alternative` sequence tried to access an item with an out-of-bounds index.
    InvalidAlternativeIndex,
    /// An `InklingError` encountered during processing.
    InklingError(Box<InklingError>),
}

#[derive(Clone, Debug)]
/// Errors related to the stack of `Knots`, `Stitches` and choices set to
/// the [`Story`][crate::story::Story].
pub enum StackError {
    /// The current stack of `Address`es is empty and a follow was requested.
    NoStack,
    /// An invalid address was used inside the system.
    ///
    /// This means that some bad assumptions have been made somewhere. Addresses are
    /// always supposed to be verified as valid before use.
    BadAddress { address: Address },
    /// No set of presented choices have been added to the system.
    NoLastChoices,
    /// No root knot was added to the stack when the `Story` was constructed.
    NoRootKnot { knot_name: String },
}

#[derive(Clone, Debug)]
/// Current node tree [`Stack`][crate::node::Stack] is incorrect.
pub enum IncorrectNodeStackError {
    /// Tried to follow through nodes with an empty stack.
    EmptyStack,
    /// Found a `Line` object where a set of branching choices should be.
    ExpectedBranchingPoint { stack_index: usize, stack: Stack },
    /// Tried to follow a branch but stack does not have an index for the follow,
    /// it is too short.
    MissingBranchIndex { stack_index: usize, stack: Stack },
    /// Stack contains an invalid index for the current node level.
    OutOfBounds {
        stack_index: usize,
        stack: Stack,
        num_items: usize,
    },
}