cargo-kconfig 0.0.4

Kconfig macro library and user interface for the Kconfig file format from the Linux Kernel
Documentation
/*
 Cargo KConfig - KConfig parser
 Copyright (C) 2022  Sjoerd van Leent

--------------------------------------------------------------------------------

Copyright Notice: Apache

Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at

   https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.

--------------------------------------------------------------------------------

Copyright Notice: GPLv2

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.

--------------------------------------------------------------------------------

Copyright Notice: MIT

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the “Software”), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

use crate::client::{ClientOpts, ConfigGetOpt, ConfigSetOpt};
use std::env::{self, Args};

use super::HelpType;

/// Specifies the options given to the command line parser. This is used to
/// determine which strategy to use for starting a client, a server or displaying
/// help text.
pub enum CliOpts {
    /// If a client is to be started, contains the options for a client
    Command(ClientOpts),

    /// If help is requests, contains what kind of help is requested. If None
    /// is set, generic help is requested, explaining what options are available.
    Help(ParseError, HelpType),
}

/// A ParseError is set as part of the Help CliOpts enumerator item to
/// indicate why the help should be printed, and what exit code after printing
/// the help should be returned to the invoking process.
pub struct ParseError(i8);

/// Indicates no parsing error has occurred
pub const NO_PARSE_ERROR: ParseError = ParseError(0);

/// Indicates a generic parse error has occurred
pub const PARSE_ERROR: ParseError = ParseError(1);

impl ParseError {
    /// Returns the code which should be used as exit code towards the
    /// invoking process
    pub fn code(self) -> i32 {
        self.0 as i32
    }
}

impl CliOpts {
    /// Parses the command line arguments into a CliOpts struct. If parsing
    /// was erroneous, shows an error, and returns the closest possible Help
    /// text for the user to determine which error was made.
    pub fn parse() -> CliOpts {
        let mut args = env::args();

        // The first argument should be skipped
        args.next();

        // The second argument contains the type of command to expect, this
        // can either be client or server. If no command is specified, or an
        // invalid command is specified, help should be displayed. Also,
        // if an invalid command is specified, an error should be displayed
        // as such.
        let cmd = args.next();

        match cmd {
            Some(cmdname) => {
                if cmdname == "help" {
                    Self::Help(PARSE_ERROR, HelpType::None)
                } else {
                    dispatcher::dispatch(&cmdname, &mut args)
                }
            }
            None => Self::Help(NO_PARSE_ERROR, HelpType::None),
        }
    }

    fn parse_config_set(args: &mut Args) -> CliOpts {
        let subcommand = args.next();
        let setting = args.next();

        match (subcommand, setting) {
            (Some(option), Some(value)) => match option.as_str() {
                "input" => Self::Command(ClientOpts::ConfigSet(ConfigSetOpt::Kconfig(
                    value.to_string(),
                ))),
                "output" => Self::Command(ClientOpts::ConfigSet(ConfigSetOpt::Dotconfig(
                    value.to_string(),
                ))),
                _ => {
                    eprintln!("⛔ Option {option} does not exist");
                    CliOpts::Help(PARSE_ERROR, HelpType::Set)
                }
            },
            (Some(option), _) => match option.as_str() {
                "input" => {
                    Self::Command(ClientOpts::ConfigSet(ConfigSetOpt::Kconfig("".to_owned())))
                }
                "output" => Self::Command(ClientOpts::ConfigSet(ConfigSetOpt::Dotconfig(
                    "".to_owned(),
                ))),
                "--help" => CliOpts::Help(NO_PARSE_ERROR, HelpType::Set),
                _ => {
                    eprintln!(
                        "⛔ Wrong amount of arguments to set command, expected two arguments"
                    );
                    CliOpts::Help(PARSE_ERROR, HelpType::Set)
                }
            },
            _ => {
                eprintln!("⛔ Wrong amount of arguments to set command, expected two arguments");
                CliOpts::Help(PARSE_ERROR, HelpType::Set)
            }
        }
    }

    fn parse_config_get(args: &mut Args) -> CliOpts {
        let subcommand = args.next();

        match subcommand {
            Some(option) => match option.as_str() {
                "input" => Self::Command(ClientOpts::ConfigGet(ConfigGetOpt::Kconfig)),
                "output" => Self::Command(ClientOpts::ConfigGet(ConfigGetOpt::Dotconfig)),
                "list" => Self::Command(ClientOpts::ConfigGet(ConfigGetOpt::List)),
                "--help" => CliOpts::Help(NO_PARSE_ERROR, HelpType::Get),
                _ => {
                    eprintln!("⛔ Option {option} does not exist");
                    CliOpts::Help(PARSE_ERROR, HelpType::Get)
                }
            },
            _ => {
                eprintln!("⛔ Wrong amount of arguments to get command, expected two arguments");
                CliOpts::Help(PARSE_ERROR, HelpType::Get)
            }
        }
    }

    fn parse_list(args: &mut Args) -> CliOpts {
        let result: Vec<String> = args.collect();

        for v in &result {
            if v == "--help" {
                return CliOpts::Help(NO_PARSE_ERROR, HelpType::List);
            }
        }

        CliOpts::Command(ClientOpts::List(result))
    }

    fn parse_info(args: &mut Args) -> CliOpts {
        let value = args.next();
        match value {
            Some(value) => {
                if value == "--help" {
                    CliOpts::Help(NO_PARSE_ERROR, HelpType::Info)
                } else {
                    CliOpts::Command(ClientOpts::Info(value))
                }
            }
            _ => {
                eprintln!("⛔ Wrong amount of arguments to info command, expected one argument");
                CliOpts::Help(PARSE_ERROR, HelpType::Info)
            }
        }
    }

    fn parse_update(args: &mut Args) -> CliOpts {
        let value = args.next();
        match value {
            Some(value) => {
                if value == "--help" {
                    CliOpts::Help(NO_PARSE_ERROR, HelpType::Update)
                } else {
                    CliOpts::Command(ClientOpts::Update(value))
                }
            }
            _ => {
                eprintln!("⛔ Wrong amount of arguments to update command, expected one argument");
                CliOpts::Help(PARSE_ERROR, HelpType::Update)
            }
        }
    }
}

mod dispatcher {
    use std::{collections::HashMap, env::Args, sync::Once};

    use crate::cli::{HelpType, PARSE_ERROR};

    use super::CliOpts;

    #[derive(Clone)]
    struct Dispatched {
        callback: fn(&mut Args) -> CliOpts,
    }

    /// Dispatches a given command to the appropriate helper, defined
    /// by the command name.
    pub(super) fn dispatch(cmdname: &str, args: &mut Args) -> CliOpts {
        match get_subcommands().get(cmdname) {
            Some(dispatched) => (dispatched.callback)(args),
            None => {
                eprintln!("⛔ Invalid command {cmdname} received");
                CliOpts::Help(PARSE_ERROR, HelpType::None)
            }
        }
    }

    static mut SUBCOMMANDS: Option<HashMap<String, Dispatched>> = None;
    static START: Once = Once::new();

    fn get_subcommands() -> &'static HashMap<String, Dispatched> {
        START.call_once(|| unsafe {
            let mut map = HashMap::new();
            add_command(&mut map, "config-set", super::CliOpts::parse_config_set);
            add_command(&mut map, "config-get", super::CliOpts::parse_config_get);
            add_command(&mut map, "list", super::CliOpts::parse_list);
            add_command(&mut map, "ls", super::CliOpts::parse_list);
            add_command(&mut map, "info", super::CliOpts::parse_info);
            add_command(&mut map, "update", super::CliOpts::parse_update);
            SUBCOMMANDS = Some(map);
        });

        unsafe {
            match &SUBCOMMANDS {
                Some(subcommands) => subcommands,
                None => panic!("⛔ Subcommand-map not initialized"),
            }
        }
    }

    fn add_command(
        map: &mut HashMap<String, Dispatched>,
        name: &str,
        callback: fn(&mut Args) -> CliOpts,
    ) {
        map.insert(name.to_owned(), Dispatched { callback });
    }
}