Files
swift-hpa/Sources/hpa/Internal/CommandConfigurationExtensions.swift

275 lines
7.7 KiB
Swift

import ArgumentParser
import Rainbow
extension CommandConfiguration {
static func create(
commandName: String,
abstract: String,
usesExtraArgs: Bool = true,
discussion: Discussion
) -> Self {
.init(
commandName: commandName,
abstract: createAbstract(abstract),
discussion: discussion.render()
)
}
static func create(
commandName: String,
abstract: String,
usesExtraArgs: Bool = true,
discussion nodes: [Node]
) -> Self {
.init(
commandName: commandName,
abstract: createAbstract(abstract),
discussion: Discussion(nodes: nodes, usesExtraArgs: usesExtraArgs).render()
)
}
static func playbook(
commandName: String,
abstract: String,
parentCommand: String? = nil,
examples: (label: String, example: String)...
) -> Self {
.create(
commandName: commandName,
abstract: abstract,
discussion: .default(
usesExtraArgs: true,
parentCommand: parentCommand,
examples: examples,
seeAlso: .seeAlso(label: "Ansible playbook options.", command: "ansible-playbook")
)
)
}
}
func createAbstract(_ string: String) -> String {
"\(string.blue)"
}
struct Discussion {
let nodes: [Node]
static func `default`(
usesExtraArgs: Bool,
parentCommand: String?,
examples: [(label: String, example: String)],
seeAlso: Node?
) -> Self {
var nodes = Array.defaultNodes + examples.nodes(parentCommand)
if let seeAlso { nodes.append(seeAlso) }
if usesExtraArgs && examples.count > 0 { nodes.append(.important(label: Constants.importantExtraArgsNote)) }
return .init(
nodes: nodes,
usesExtraArgs: usesExtraArgs
)
}
init(usesExtraArgs: Bool = true, _ nodes: Node...) {
self.init(nodes: nodes, usesExtraArgs: usesExtraArgs)
}
init(nodes: [Node], usesExtraArgs: Bool = true) {
var nodes = nodes
if let firstExampleIndex = nodes.firstIndex(where: \.isExampleNode) {
nodes.insert(.exampleHeading, at: firstExampleIndex)
}
if usesExtraArgs {
if let lastExampleIndex = nodes.lastIndex(where: \.isExampleNode) {
guard case let .example(.container(exampleNodes, separator)) = nodes[lastExampleIndex] else {
// this should never happen.
print(nodes[lastExampleIndex])
fatalError()
}
// Get the last element of the example container, which will be the
// text for the example.
guard let exampleNode = exampleNodes.last else {
print(exampleNodes)
fatalError()
}
// replace the first element, which is the header so that we can
// replace it with a new header.
nodes.insert(
.example(
.container(
[
.exampleLabel("Passing extra args."),
.container([exampleNode, .text("-- --vault-id \"myId@$SCRIPTS/vault-gopass-client\"")], separator: " ")
],
separator: separator
)),
at: nodes.index(after: lastExampleIndex)
)
}
}
self.nodes = nodes
}
func render() -> String { nodes.render() }
}
enum Node {
/// A container that holds onto other nodes to be rendered.
indirect case container(_ nodes: [Node], separator: String = "\n\n")
/// A container to identify example nodes, so some other nodes can get injected
/// when this is rendered.
///
/// NOTE: Things blow up if this is not used correctly, at least when using the `Discussion`
/// to render the nodes, so this is not safe to create aside from the static methods that
/// create an example node. If not using in the context of a `Discussion` then it's fine to
/// use, as it doesn't do anything except stand as an type identifier.
indirect case example(_ node: Node)
/// A root node that renders the given string without modification.
case text(_ text: String)
static func styledText(_ text: String, color: NamedColor? = nil, styles: [Style]? = nil) -> Self {
var string = text
if let color {
string = string.applyingColor(color)
}
if let styles {
for style in styles {
string = string.applyingStyle(style)
}
}
return .text(string)
}
static func styledText(_ text: String, color: NamedColor? = nil, style: Style) -> Self {
.styledText(text, color: color, styles: [style])
}
static func header(_ text: String) -> Self {
.styledText(text, color: .yellow, styles: [.bold])
}
static func section(header: Node, content: Node, inline: Bool = false) -> Self {
.container([header, content], separator: inline ? " " : "\n")
}
static func section(header: String, label: Node, inline: Bool = false) -> Self {
.section(header: .header(header), content: label, inline: inline)
}
static func important(label: String) -> Self {
.section(
header: .text("IMPORTANT NOTE:".red.bold.underline),
content: .text(label.red.italic),
inline: true
)
}
static func note(label: String, labelInline: Bool = true) -> Self {
.section(header: "NOTE:", label: .text(label.italic), inline: labelInline)
}
static func container(separator: String = "\n\n", _ nodes: Node...) -> Self {
.container(nodes, separator: separator)
}
static func shellCommand(_ text: String, symbol: String = " $") -> Self {
.text("\(symbol) \(text)")
}
static func seeAlso(label: String, command: String, appendHelpToCommand: Bool = true) -> Self {
.container(
.container([.header("See Also:"), .text(label.italic)], separator: " "),
.shellCommand("\(appendHelpToCommand ? command + " --help" : command)")
)
}
static var exampleHeading: Self {
.section(header: "Examples:", label: .text("Some common examples."), inline: true)
}
var isExampleNode: Bool {
if case .example = self { return true }
return false
}
/// Renders an example usage of the command.
static func example(label: String, example: String, parentCommand: String?) -> Self {
let string: String
if let parent = parentCommand {
string = "\(Constants.appName) \(parent) \(example)"
} else {
string = "\(Constants.appName) \(example)"
}
return .example(.section(
header: .exampleLabel(label),
content: .shellCommand(string, symbol: " $")
))
}
static func exampleLabel(_ label: String) -> Node {
.text(" \(label.green.italic)")
}
func render() -> String {
switch self {
case let .container(nodes, separator):
return nodes.render(separator: separator)
case let .text(text):
return text
case let .example(node):
return node.render()
}
}
}
extension Node: ExpressibleByStringLiteral {
typealias StringLiteralType = String
init(stringLiteral value: String) {
self = .text(value)
}
}
extension Node: ExpressibleByArrayLiteral {
typealias ArrayLiteralElement = Node
init(arrayLiteral elements: Node...) {
self = .container(elements)
}
}
private extension Array where Element == (label: String, example: String) {
func nodes(_ parentCommand: String?) -> [Node] {
map { .example(label: $0.label, example: $0.example, parentCommand: parentCommand) }
}
}
extension Array where Element == Node {
static var defaultNodes: Self {
[.note(label: "Most options are not required if you have a configuration file setup.")]
}
fileprivate func render(separator: String = "\n\n") -> String {
map {
// Strip of any new-line characters from the last section of the rendered string
// of the node. This allows us to have a consistent single new-line between each
// rendered node.
var string = $0.render()[...]
while string.hasSuffix("\n") {
string = string.dropLast()
}
return String(string)
}.joined(separator: separator)
}
}