feat: Working on node builder
This commit is contained in:
@@ -21,6 +21,7 @@ let package = Package(
|
|||||||
name: "hpa",
|
name: "hpa",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"CliClient",
|
"CliClient",
|
||||||
|
"CliDoc",
|
||||||
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
||||||
.product(name: "Dependencies", package: "swift-dependencies"),
|
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||||
.product(name: "ShellClient", package: "swift-shell-client")
|
.product(name: "ShellClient", package: "swift-shell-client")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import ArgumentParser
|
|||||||
|
|
||||||
public extension CommandConfiguration {
|
public extension CommandConfiguration {
|
||||||
|
|
||||||
|
/// Use `NodeBuilder` syntax for generating the abstract and discussion parameters.
|
||||||
init(
|
init(
|
||||||
commandName: String? = nil,
|
commandName: String? = nil,
|
||||||
abstract: Abstract,
|
abstract: Abstract,
|
||||||
|
|||||||
58
Sources/CliDoc/Modifiers/LabeledContentStyle.swift
Normal file
58
Sources/CliDoc/Modifiers/LabeledContentStyle.swift
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
public enum LabelContentStyle: Sendable {
|
||||||
|
case inline
|
||||||
|
case `default`
|
||||||
|
case custom(any NodeRepresentable)
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension NodeRepresentable {
|
||||||
|
@inlinable
|
||||||
|
func labeledContentStyle(_ style: LabelContentStyle) -> any NodeRepresentable {
|
||||||
|
modifier(LabeledContentStyleModifier(style: style))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
struct LabeledContentStyleModifier: NodeModifier {
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
let style: LabelContentStyle
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
init(style: LabelContentStyle) {
|
||||||
|
self.style = style
|
||||||
|
}
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
func render(_ node: any NodeRepresentable) -> any NodeRepresentable {
|
||||||
|
if let many = node as? _ManyNode {
|
||||||
|
var nodes = many.nodes
|
||||||
|
print("nodes: \(nodes)")
|
||||||
|
for (idx, node) in nodes.enumerated() {
|
||||||
|
if let labeled = node as? LabeledContent {
|
||||||
|
let newNode = labeled.applyingContentStyle(style)
|
||||||
|
nodes[idx] = newNode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _ManyNode(nodes, separator: many.separator)
|
||||||
|
} else if let labeled = node as? LabeledContent {
|
||||||
|
return labeled.applyingContentStyle(style)
|
||||||
|
}
|
||||||
|
print("no many or labeled nodes found in: \(node)")
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension LabeledContent {
|
||||||
|
|
||||||
|
func applyingContentStyle(_ style: LabelContentStyle) -> Self {
|
||||||
|
switch style {
|
||||||
|
case .inline:
|
||||||
|
return .init(separator: " ") { label } content: { content }
|
||||||
|
case .default:
|
||||||
|
return .init(separator: "\n") { label } content: { content }
|
||||||
|
case let .custom(custom):
|
||||||
|
return .init(separator: custom) { label } content: { content }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
Sources/CliDoc/Node.swift
Normal file
26
Sources/CliDoc/Node.swift
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// swiftlint:disable type_name
|
||||||
|
public protocol Node: NodeRepresentable {
|
||||||
|
associatedtype _Body: NodeRepresentable
|
||||||
|
typealias Body = _Body
|
||||||
|
|
||||||
|
@NodeBuilder
|
||||||
|
var body: Body { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
// swiftlint:enable type_name
|
||||||
|
|
||||||
|
public extension Node {
|
||||||
|
@inlinable
|
||||||
|
func render() -> String {
|
||||||
|
body.render()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Foo: Node {
|
||||||
|
|
||||||
|
public init() {}
|
||||||
|
|
||||||
|
public var body: some NodeRepresentable {
|
||||||
|
LabeledContent("Foo") { "Bar" }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,16 +16,16 @@ public enum NodeBuilder {
|
|||||||
.init(components, separator: .empty())
|
.init(components, separator: .empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func buildEither<N0: NodeRepresentable, N1: NodeRepresentable>(
|
public static func buildEither<N: NodeRepresentable>(
|
||||||
first component: N0
|
first component: N
|
||||||
) -> _ConditionalNode<N0, N1> {
|
) -> N {
|
||||||
.first(component)
|
component
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func buildEither<N0: NodeRepresentable, N1: NodeRepresentable>(
|
public static func buildEither<N: NodeRepresentable>(
|
||||||
second component: N1
|
second component: N
|
||||||
) -> _ConditionalNode<N0, N1> {
|
) -> N {
|
||||||
.second(component)
|
component
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func buildBlock<N: NodeRepresentable>(_ components: N...) -> _ManyNode {
|
public static func buildBlock<N: NodeRepresentable>(_ components: N...) -> _ManyNode {
|
||||||
@@ -37,8 +37,13 @@ public enum NodeBuilder {
|
|||||||
// expression.eraseToAnyNode()
|
// expression.eraseToAnyNode()
|
||||||
// }
|
// }
|
||||||
|
|
||||||
public static func buildOptional<N: NodeRepresentable>(_ component: N?) -> AnyNode {
|
public static func buildOptional<N: NodeRepresentable>(_ component: N?) -> _ConditionalNode<N, Text> {
|
||||||
component?.eraseToAnyNode() ?? AnyNode { Text.empty() }
|
switch component {
|
||||||
|
case let .some(node):
|
||||||
|
return .first(node)
|
||||||
|
case .none:
|
||||||
|
return .second(.empty())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func buildFinalResult<N: NodeRepresentable>(_ component: N) -> N {
|
public static func buildFinalResult<N: NodeRepresentable>(_ component: N) -> N {
|
||||||
@@ -79,15 +84,18 @@ public struct _ManyNode: NodeRepresentable {
|
|||||||
@usableFromInline
|
@usableFromInline
|
||||||
init(
|
init(
|
||||||
_ nodes: [any NodeRepresentable],
|
_ nodes: [any NodeRepresentable],
|
||||||
separator: AnyNode = Text.empty().eraseToAnyNode()
|
separator: any NodeRepresentable = "\n" as any NodeRepresentable
|
||||||
) {
|
) {
|
||||||
self.nodes = nodes
|
// Flatten the nodes.
|
||||||
self.separator = separator
|
var allNodes = [any NodeRepresentable]()
|
||||||
}
|
for node in nodes {
|
||||||
|
if let many = node as? Self {
|
||||||
@inlinable
|
allNodes += many.nodes
|
||||||
public init(_ nodes: [any NodeRepresentable], separator: any NodeRepresentable) {
|
} else {
|
||||||
self.nodes = nodes
|
allNodes.append(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.nodes = allNodes
|
||||||
self.separator = separator
|
self.separator = separator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
30
Sources/CliDoc/Nodes/Section.swift
Normal file
30
Sources/CliDoc/Nodes/Section.swift
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
public struct Section: NodeRepresentable {
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
let content: any NodeRepresentable
|
||||||
|
|
||||||
|
public init(@NodeBuilder content: () -> any NodeRepresentable) {
|
||||||
|
self.content = content()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func render() -> String {
|
||||||
|
guard let many = content as? _ManyNode else {
|
||||||
|
return content.render() + "\n\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
let node = _ManyNode(many.nodes, separator: "\n\n")
|
||||||
|
return node.render()
|
||||||
|
|
||||||
|
// var output = ""
|
||||||
|
// let lastIndex = many.nodes.count - 1
|
||||||
|
//
|
||||||
|
// for (idx, content) in many.nodes.enumerated() {
|
||||||
|
// if idx != lastIndex {
|
||||||
|
// output += content.render() + "\n\n"
|
||||||
|
// } else {
|
||||||
|
// output += content.render() + "\n"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return output
|
||||||
|
}
|
||||||
|
}
|
||||||
41
Sources/CliDoc/Nodes/ShellCommand.swift
Normal file
41
Sources/CliDoc/Nodes/ShellCommand.swift
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
public struct ShellCommand: NodeRepresentable {
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
let symbol: any NodeRepresentable
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
let command: any NodeRepresentable
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public init(
|
||||||
|
@NodeBuilder symbol: () -> any NodeRepresentable,
|
||||||
|
@NodeBuilder command: () -> any NodeRepresentable
|
||||||
|
) {
|
||||||
|
self.symbol = symbol()
|
||||||
|
self.command = command()
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public init(
|
||||||
|
symbol: String = " $",
|
||||||
|
@NodeBuilder command: () -> any NodeRepresentable
|
||||||
|
) {
|
||||||
|
self.init { symbol } command: { command() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public init(
|
||||||
|
symbol: String = " $",
|
||||||
|
command: String
|
||||||
|
) {
|
||||||
|
self.init { symbol } command: { command }
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public func render() -> String {
|
||||||
|
Group(separator: " ") {
|
||||||
|
symbol
|
||||||
|
command
|
||||||
|
}.render()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ struct CreateCommand: AsyncParsableCommand {
|
|||||||
|
|
||||||
static let commandName = "create"
|
static let commandName = "create"
|
||||||
|
|
||||||
static let configuration = CommandConfiguration.playbook(
|
static let configuration = CommandConfiguration.playbook2(
|
||||||
commandName: commandName,
|
commandName: commandName,
|
||||||
abstract: "Create a home performance assesment project.",
|
abstract: "Create a home performance assesment project.",
|
||||||
examples: (
|
examples: (
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import ArgumentParser
|
import ArgumentParser
|
||||||
|
import CliDoc
|
||||||
import Rainbow
|
import Rainbow
|
||||||
|
|
||||||
extension CommandConfiguration {
|
extension CommandConfiguration {
|
||||||
@@ -29,6 +30,25 @@ extension CommandConfiguration {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func playbook2(
|
||||||
|
commandName: String,
|
||||||
|
abstract: String,
|
||||||
|
parentCommand: String? = nil,
|
||||||
|
examples: (label: String, example: String)...
|
||||||
|
) -> Self {
|
||||||
|
.init(
|
||||||
|
commandName: commandName,
|
||||||
|
abstract: Abstract { abstract.blue },
|
||||||
|
usage: """
|
||||||
|
\(Constants.appName)\(parentCommand != nil ? " \(parentCommand!)" : "") \(commandName)
|
||||||
|
""".blue.bold.italic
|
||||||
|
+ " [OPTIONS]".green
|
||||||
|
+ " [ARGUMENTS]".cyan
|
||||||
|
+ " --" + " [EXTRA-OPTIONS...]".magenta,
|
||||||
|
discussion: CliDoc.Discussion.playbook(examples: examples)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
static func playbook(
|
static func playbook(
|
||||||
commandName: String,
|
commandName: String,
|
||||||
abstract: String,
|
abstract: String,
|
||||||
@@ -52,6 +72,7 @@ func createAbstract(_ string: String) -> String {
|
|||||||
"\(string.blue)"
|
"\(string.blue)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Remove
|
||||||
struct Discussion {
|
struct Discussion {
|
||||||
let nodes: [Node]
|
let nodes: [Node]
|
||||||
|
|
||||||
|
|||||||
120
Sources/hpa/Internal/Discussion+playbook.swift
Normal file
120
Sources/hpa/Internal/Discussion+playbook.swift
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import CliDoc
|
||||||
|
|
||||||
|
extension CliDoc.Discussion {
|
||||||
|
static func playbook(
|
||||||
|
examples: [(label: String, example: String)]
|
||||||
|
) -> Self {
|
||||||
|
.init {
|
||||||
|
Section {
|
||||||
|
Note.mostOptionsNotRequired
|
||||||
|
Examples(examples: examples)
|
||||||
|
SeeAlso(label: "Ansible playbook options.", command: "ansible-playbook --help")
|
||||||
|
ImportantNote.passingExtraArgs
|
||||||
|
}
|
||||||
|
.labeledContentStyle(.custom("foo:"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ShellCommand {
|
||||||
|
static func hpaCommand(_ command: String) -> Self {
|
||||||
|
.init(command: "\(Constants.appName) \(command)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SeeAlso: NodeRepresentable {
|
||||||
|
|
||||||
|
let node: any NodeRepresentable
|
||||||
|
|
||||||
|
init(label: String, command: String) {
|
||||||
|
self.node = Group(separator: "\n") {
|
||||||
|
Note("SEE ALSO:") {
|
||||||
|
label
|
||||||
|
}
|
||||||
|
ShellCommand(command: command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func render() -> String {
|
||||||
|
node.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Examples: NodeRepresentable {
|
||||||
|
typealias Example = (label: String, example: String)
|
||||||
|
let examples: [Example]
|
||||||
|
|
||||||
|
func render() -> String {
|
||||||
|
Group(separator: "\n") {
|
||||||
|
Note("EXAMPLES:") { "Common usage examples." }
|
||||||
|
for (label, command) in examples {
|
||||||
|
LabeledContent("\t\(label.green.bold)", separator: "\n") {
|
||||||
|
ShellCommand.hpaCommand(command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let first = examples.first {
|
||||||
|
LabeledContent("\n\tPassing extra options.".green.bold, separator: "\n") {
|
||||||
|
ShellCommand.hpaCommand("\(first.example) -- --vault-id \"myId@$SCRIPTS/vault-gopass-client\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.render()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Note: NodeRepresentable {
|
||||||
|
|
||||||
|
let node: any NodeRepresentable
|
||||||
|
|
||||||
|
init(
|
||||||
|
_ label: String = "NOTE:",
|
||||||
|
@NodeBuilder _ content: () -> any NodeRepresentable
|
||||||
|
) {
|
||||||
|
self.node = LabeledContent(
|
||||||
|
separator: " ",
|
||||||
|
label: { label.yellow.bold },
|
||||||
|
content: { content().style(.italic) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func render() -> String {
|
||||||
|
node.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
static var mostOptionsNotRequired: Self {
|
||||||
|
.init {
|
||||||
|
"Most options are not required if you have a configuration file setup."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Fix the text.
|
||||||
|
struct ImportantNote: NodeRepresentable {
|
||||||
|
|
||||||
|
let node: any NodeRepresentable
|
||||||
|
|
||||||
|
init(
|
||||||
|
_ label: String = "IMPORTANT NOTE:",
|
||||||
|
@NodeBuilder _ content: () -> any NodeRepresentable
|
||||||
|
) {
|
||||||
|
self.node = LabeledContent(
|
||||||
|
separator: "\n",
|
||||||
|
label: { label.red.bold.underline },
|
||||||
|
content: { content().style(.italic) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func render() -> String {
|
||||||
|
node.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
static var passingExtraArgs: Self {
|
||||||
|
.init {
|
||||||
|
"""
|
||||||
|
Any extra options passed to the underlying command need to come after
|
||||||
|
`--` otherwise an "Unknown option" error will occur. See above example of passing
|
||||||
|
extra options.
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,10 +13,9 @@ final class CliDocTests: XCTestCase {
|
|||||||
|
|
||||||
func testStringChecks() {
|
func testStringChecks() {
|
||||||
let expected = "Foo".green.bold
|
let expected = "Foo".green.bold
|
||||||
let notexpected = "Foo"
|
|
||||||
|
|
||||||
XCTAssert("Foo".green.bold == expected)
|
XCTAssert("Foo".green.bold == expected)
|
||||||
XCTAssert("Foo".green.bold != notexpected)
|
XCTAssert(expected != "Foo")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRepeatingModifier() {
|
func testRepeatingModifier() {
|
||||||
@@ -96,4 +95,35 @@ final class CliDocTests: XCTestCase {
|
|||||||
"""
|
"""
|
||||||
XCTAssert(node.render() == expected)
|
XCTAssert(node.render() == expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testShellCommand() {
|
||||||
|
let node = ShellCommand {
|
||||||
|
"ls -lah"
|
||||||
|
}
|
||||||
|
let expected = " $ ls -lah"
|
||||||
|
XCTAssert(node.render() == expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDiscussion() {
|
||||||
|
let node = Discussion {
|
||||||
|
Group(separator: "\n") {
|
||||||
|
LabeledContent(separator: " ") {
|
||||||
|
"NOTE:".yellow.bold
|
||||||
|
} content: {
|
||||||
|
"Foo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let expected = """
|
||||||
|
\("NOTE:".yellow.bold) Foo
|
||||||
|
"""
|
||||||
|
|
||||||
|
XCTAssert(node.render() == expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFooNode() {
|
||||||
|
let foo = Foo()
|
||||||
|
XCTAssertNotNil(foo.body as? LabeledContent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user