fixture_manager/
lib.rs

1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4// This is fired by the arg_enum macro. We really need to upgrade to clap v4 at some point.
5#![allow(clippy::useless_vec)]
6
7pub mod context;
8pub mod hakari_toml;
9pub mod summaries;
10
11use crate::{
12    context::{ContextImpl, GenerateContext},
13    hakari_toml::HakariTomlContext,
14    summaries::*,
15};
16use anyhow::{anyhow, bail, Result};
17use clap::arg_enum;
18use fixtures::json::JsonFixture;
19use structopt::StructOpt;
20
21#[derive(Debug, StructOpt)]
22pub struct FixtureManager {
23    // TODO: add global options
24    #[structopt(subcommand)]
25    cmd: Command,
26}
27
28impl FixtureManager {
29    pub fn exec(self) -> Result<()> {
30        match self.cmd {
31            Command::List => list(),
32            Command::GenerateSummaries(opts) => opts.exec(),
33            Command::GenerateHakari(opts) => opts.exec(),
34        }
35    }
36}
37
38#[derive(Debug, StructOpt)]
39enum Command {
40    #[structopt(name = "list")]
41    /// List fixtures
42    List,
43    /// Generate summaries
44    GenerateSummaries(GenerateSummariesOpts),
45    /// Generate Hakari outputs
46    GenerateHakari(GenerateHakariOpts),
47}
48
49pub fn list() -> Result<()> {
50    for (name, fixture) in JsonFixture::all_fixtures().iter() {
51        println!("{}: {}", name, fixture.workspace_path());
52    }
53
54    Ok(())
55}
56
57#[derive(Debug, StructOpt)]
58pub struct GenerateSummariesOpts {
59    /// Number of summaries to generate
60    #[structopt(long, default_value = Self::DEFAULT_COUNT_STR)]
61    pub count: usize,
62
63    #[structopt(flatten)]
64    pub generate_opts: GenerateOpts,
65}
66
67impl GenerateSummariesOpts {
68    /// The default value of the `count` field, as a string.
69    pub const DEFAULT_COUNT_STR: &'static str = "8";
70
71    /// The default value of the `count` field.
72    pub fn default_count() -> usize {
73        Self::DEFAULT_COUNT_STR
74            .parse()
75            .expect("DEFAULT_COUNT_STR should parse as a usize")
76    }
77}
78
79#[derive(Debug, StructOpt)]
80pub struct GenerateHakariOpts {
81    /// Number of options to generate
82    #[structopt(long, default_value = Self::DEFAULT_COUNT_STR)]
83    pub count: usize,
84
85    #[structopt(flatten)]
86    pub generate_opts: GenerateOpts,
87}
88
89impl GenerateHakariOpts {
90    /// The default value of the `count` field, as a string.
91    pub const DEFAULT_COUNT_STR: &'static str = "4";
92
93    /// The default value of the `count` field.
94    pub fn default_count() -> usize {
95        Self::DEFAULT_COUNT_STR
96            .parse()
97            .expect("DEFAULT_COUNT_STR should parse as a usize")
98    }
99}
100
101#[derive(Debug, StructOpt)]
102pub struct GenerateOpts {
103    /// Execution mode (check, force or generate)
104    #[structopt(long, short, possible_values = &GenerateMode::variants(), case_insensitive = true, default_value = "generate")]
105    pub mode: GenerateMode,
106
107    /// Only generate outputs for these fixtures
108    #[structopt(long)]
109    pub fixtures: Vec<String>,
110}
111
112arg_enum! {
113    #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
114    pub enum GenerateMode {
115        Generate,
116        Check,
117        Force,
118    }
119}
120
121impl GenerateSummariesOpts {
122    pub fn exec(self) -> Result<()> {
123        self.generate_opts.exec::<SummaryContext>(self.count)
124    }
125}
126
127impl GenerateHakariOpts {
128    pub fn exec(self) -> Result<()> {
129        self.generate_opts.exec::<HakariTomlContext>(self.count)
130    }
131}
132
133impl GenerateOpts {
134    pub fn exec<'g, T: ContextImpl<'g>>(self, args: T::IterArgs) -> Result<()> {
135        let fixtures: Box<dyn Iterator<Item = (&str, &JsonFixture)>> = if self.fixtures.is_empty() {
136            Box::new(
137                JsonFixture::all_fixtures()
138                    .iter()
139                    .map(|(name, fixture)| (*name, fixture)),
140            )
141        } else {
142            let fixtures = self
143                .fixtures
144                .iter()
145                .map(|name| {
146                    let fixture = JsonFixture::by_name(name)
147                        .ok_or_else(|| anyhow!("unknown fixture: {}", name))?;
148                    Ok((name.as_str(), fixture))
149                })
150                .collect::<Result<Vec<_>>>()?;
151            Box::new(fixtures.into_iter())
152        };
153
154        let mut num_changed = 0;
155
156        for (name, fixture) in fixtures {
157            println!("generating outputs for {}...", name);
158
159            let context: GenerateContext<'_, T> =
160                GenerateContext::new(fixture, &args, self.mode == GenerateMode::Force)?;
161            for item in context {
162                let item = item?;
163                let is_changed = item.is_changed();
164
165                if is_changed {
166                    num_changed += 1;
167                }
168
169                if self.mode == GenerateMode::Check {
170                    if is_changed {
171                        println!("** {}:\n{}", item.path(), item.diff());
172                    }
173
174                    continue;
175                }
176
177                if is_changed || self.mode == GenerateMode::Force {
178                    item.write_to_path()?;
179                }
180            }
181        }
182
183        if self.mode == GenerateMode::Check && num_changed > 0 {
184            bail!("{} outputs changed", num_changed);
185        }
186
187        println!("{} outputs changed", num_changed);
188
189        Ok(())
190    }
191}