├── .github
└── workflows
│ └── tests.yml
├── .gitignore
├── .swiftlint.yml
├── Brewfile
├── Brewfile.lock.json
├── CHANGELOG.md
├── Examples
├── HTML
│ ├── Murray
│ │ └── HTML
│ │ │ ├── HTML.yml
│ │ │ └── page
│ │ │ ├── Page.html.stencil
│ │ │ └── page.yml
│ ├── Murrayfile.yml
│ ├── README.md
│ ├── Skeleton.yml
│ └── index.html
└── iOS
│ └── MurrayDemo
│ ├── .gitignore
│ ├── Makefile
│ ├── Mintfile
│ ├── Murray
│ └── Project
│ │ ├── Project.yml
│ │ ├── sceneView
│ │ ├── SceneView.swift.stencil
│ │ └── sceneView.yml
│ │ └── sceneViewModel
│ │ ├── ViewModel.swift.stencil
│ │ ├── ViewModelTests.swift.stencil
│ │ └── sceneViewModel.yml
│ ├── MurrayDemo.xcodeproj
│ ├── project.pbxproj
│ └── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ ├── MurrayDemo
│ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── MainTab.swift
│ ├── MainTabViewModel.swift
│ ├── MurrayDemoApp.swift
│ └── Preview Content
│ │ └── Preview Assets.xcassets
│ │ └── Contents.json
│ ├── MurrayDemoTests
│ └── MurrayDemoTests.swift
│ ├── Murrayfile.yml
│ └── README.md
├── LICENSE
├── Makefile
├── Package.resolved
├── Package.swift
├── README.md
├── Sources
├── Murray
│ ├── Commands
│ │ ├── Clone.swift
│ │ ├── List.swift
│ │ ├── Run.swift
│ │ └── Scaffold.swift
│ ├── Menu.swift
│ ├── Strings.swift
│ └── main.swift
└── MurrayKit
│ ├── Coding
│ ├── Decoder.swift
│ ├── Encoder.swift
│ ├── JSONCoder.swift
│ ├── Parameters.swift
│ └── YAMLCoder.swift
│ ├── Commands
│ ├── Clone.swift
│ ├── Command.swift
│ ├── List.swift
│ ├── Run.swift
│ └── Scaffold.swift
│ ├── Errors.swift
│ ├── Logger
│ └── Logger.swift
│ ├── Models
│ ├── CodableFile.swift
│ ├── Content.swift
│ ├── Item
│ │ ├── Item+Writeable.swift
│ │ └── Item.swift
│ ├── MurrayFile.swift
│ ├── Package.swift
│ ├── Pipeline.swift
│ ├── Procedure
│ │ ├── PackagedProcedure.swift
│ │ └── Procedure.swift
│ ├── Repository.swift
│ ├── Resolvable.swift
│ ├── RootFile.swift
│ ├── Skeleton.swift
│ ├── Template.swift
│ └── WriteableFile.swift
│ ├── Plugin
│ ├── Plugin.swift
│ ├── PluginDataContainer.swift
│ ├── PluginExecution.swift
│ ├── PluginManager.swift
│ ├── ShellPlugin.swift
│ └── XcodePlugin.swift
│ └── Utilities
│ ├── Files+Utilities.swift
│ ├── Process+Extensions.swift
│ └── String+Extensions.swift
├── Tests
├── LinuxMain.swift
└── MurrayKitTests
│ ├── CodableTests.swift
│ ├── Commands
│ ├── CloneTests.swift
│ ├── CommandTests.swift
│ ├── ListTests.swift
│ ├── RunTests.swift
│ └── ScaffoldTests.swift
│ ├── ItemTests.swift
│ ├── Mocks
│ ├── SimpleJSON
│ │ ├── Murray
│ │ │ └── Simple
│ │ │ │ ├── Folder
│ │ │ │ ├── Subfolder
│ │ │ │ │ ├── AnotherSubfolderWith{{name}}
│ │ │ │ │ │ ├── Object.swift
│ │ │ │ │ │ └── {{name}}.swift
│ │ │ │ │ ├── Bone.swift
│ │ │ │ │ └── {{name}}.swift
│ │ │ │ └── item.yml
│ │ │ │ ├── ReplacementOnly
│ │ │ │ ├── Replacement.swift
│ │ │ │ └── item.yml
│ │ │ │ ├── Simple.json
│ │ │ │ └── SimpleItem
│ │ │ │ ├── Bone.swift
│ │ │ │ ├── Replacement.swift
│ │ │ │ └── SimpleItem.json
│ │ ├── Murrayfile
│ │ ├── Sources
│ │ │ └── Files
│ │ │ │ └── Default
│ │ │ │ ├── Test.swift
│ │ │ │ ├── Test2.swift
│ │ │ │ └── TestReplacementOnly.swift
│ │ └── Test.xcodeproj
│ │ │ └── project.pbxproj
│ ├── SimpleYaml
│ │ ├── Murray
│ │ │ ├── Simple
│ │ │ │ ├── ReplacementOnly
│ │ │ │ │ ├── Replacement.swift
│ │ │ │ │ └── item.yml
│ │ │ │ ├── Simple.yml
│ │ │ │ └── SimpleItem
│ │ │ │ │ ├── .gitKeep
│ │ │ │ │ ├── Bone.swift
│ │ │ │ │ ├── File
│ │ │ │ │ ├── Replacement.swift
│ │ │ │ │ └── SimpleItem.yml
│ │ │ └── SimpleCopy
│ │ │ │ ├── Simple.yml
│ │ │ │ └── SimpleItem
│ │ │ │ ├── Bone.swift
│ │ │ │ ├── Replacement.swift
│ │ │ │ └── SimpleItem.yml
│ │ ├── Murrayfile.yml
│ │ ├── Sources
│ │ │ └── Files
│ │ │ │ └── Default
│ │ │ │ ├── Test.swift
│ │ │ │ ├── Test2.swift
│ │ │ │ └── TestReplacementOnly.swift
│ │ └── Test.xcodeproj
│ │ │ └── project.pbxproj
│ ├── Skeleton
│ │ ├── Skeleton.yml
│ │ ├── Sources
│ │ │ └── Swift.swift
│ │ ├── main.swift
│ │ └── {{name}}
│ │ │ └── Hello{{name|uppercase}}.swift
│ ├── SkeletonInSubfolder
│ │ └── Subfolder
│ │ │ ├── Skeleton.yml
│ │ │ ├── Sources
│ │ │ └── Swift.swift
│ │ │ └── {{name}}
│ │ │ └── Hello{{name|uppercase}}.swift
│ └── WrongMurrayfile
│ │ └── Murrayfile.yml
│ ├── MurrayfileTests.swift
│ ├── PipelineTests.swift
│ ├── ProcedureTests.swift
│ ├── TemplateTests.swift
│ ├── Utilities
│ ├── MurrayTestCase.swift
│ └── Scenario.swift
│ └── WriteableFileTests.swift
└── docs
└── diagram.svg
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: MurrayKit Tests
2 | on:
3 | push:
4 | branches: ["main"]
5 | paths:
6 | - Sources
7 | - Tests
8 | pull_request:
9 | branches: ["main"]
10 | types: [opened, edited, reopened, synchronize]
11 | paths:
12 | - Sources
13 | - Tests
14 | jobs:
15 | macOS:
16 | runs-on: macOS-latest
17 | steps:
18 | - uses: actions/checkout@v2
19 | - name: Lint
20 | run: swiftlint lint --strict
21 | - name: Resolve
22 | run: swift package resolve
23 | - name: Build
24 | run: swift build
25 | - name: Run tests
26 | run: swift test 2>&1 | xcpretty
27 | linux:
28 | runs-on: ubuntu-latest
29 | # container: swift:5.3
30 | steps:
31 | - uses: actions/checkout@v2
32 | - name: Resolve
33 | run: swift package resolve
34 | - name: Build
35 | run: swift build
36 | - name: Run tests
37 | run: swift test --enable-test-discovery 2>&1
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | .swiftpm
6 | .vscode
7 | MurrayBar2
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | disabled_rules:
2 | - trailing_whitespace
3 | - blanket_disable_command
4 | opt_in_rules:
5 | - empty_count
6 | included:
7 | - Sources
8 | - Tests
9 | excluded:
10 | - Tests/MurrayKitTests/Mocks
11 |
12 | force_cast: error # implicitly
13 | force_try:
14 | severity: error # explicitly
15 |
16 | line_length: 155
17 | function_body_length: 80
18 |
19 | type_body_length:
20 | warning: 300
21 | error: 400
22 | file_length:
23 | warning: 300
24 | error: 700
25 | type_name:
26 | min_length: 3
27 | max_length:
28 | warning: 50
29 | error: 60
30 | nesting:
31 | type_level: 3
32 | identifier_name:
33 | min_length:
34 | error: 2
35 | excluded:
36 | - id
37 | - URL
38 | - to
39 | reporter: "xcode"
40 |
41 |
--------------------------------------------------------------------------------
/Brewfile:
--------------------------------------------------------------------------------
1 | # Swiftlint
2 | brew 'swiftlint'
3 |
4 | # SwiftFormat
5 | brew 'swiftformat'
6 | brew 'swiftlint'
--------------------------------------------------------------------------------
/Brewfile.lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "entries": {
3 | "brew": {
4 | "swiftlint": {
5 | "version": "0.48.0",
6 | "bottle": {
7 | "rebuild": 0,
8 | "root_url": "https://ghcr.io/v2/homebrew/core",
9 | "files": {
10 | "arm64_monterey": {
11 | "cellar": ":any_skip_relocation",
12 | "url": "https://ghcr.io/v2/homebrew/core/swiftlint/blobs/sha256:4942fa227daafa9aaf14197f3a5972e4b057dd168e592d236d98f7f337afbc25",
13 | "sha256": "4942fa227daafa9aaf14197f3a5972e4b057dd168e592d236d98f7f337afbc25"
14 | },
15 | "arm64_big_sur": {
16 | "cellar": ":any_skip_relocation",
17 | "url": "https://ghcr.io/v2/homebrew/core/swiftlint/blobs/sha256:4e3a73e893d68ed38271e857b81720958bb55cbbb05a3d5bf31b9b67f7eddf52",
18 | "sha256": "4e3a73e893d68ed38271e857b81720958bb55cbbb05a3d5bf31b9b67f7eddf52"
19 | },
20 | "monterey": {
21 | "cellar": ":any_skip_relocation",
22 | "url": "https://ghcr.io/v2/homebrew/core/swiftlint/blobs/sha256:381d414359505bc72348f151751bb77a3c402e9d5ff652579687e3c5391d6872",
23 | "sha256": "381d414359505bc72348f151751bb77a3c402e9d5ff652579687e3c5391d6872"
24 | },
25 | "big_sur": {
26 | "cellar": ":any_skip_relocation",
27 | "url": "https://ghcr.io/v2/homebrew/core/swiftlint/blobs/sha256:74d707b9989d622af952db542b786a3ebecc3b8be53762fce8a25ac075f6da1a",
28 | "sha256": "74d707b9989d622af952db542b786a3ebecc3b8be53762fce8a25ac075f6da1a"
29 | },
30 | "x86_64_linux": {
31 | "cellar": "/home/linuxbrew/.linuxbrew/Cellar",
32 | "url": "https://ghcr.io/v2/homebrew/core/swiftlint/blobs/sha256:8aa64a4dce96e961898e41c2240b192a030575571ae9fe0d3a41bb79424fe9b1",
33 | "sha256": "8aa64a4dce96e961898e41c2240b192a030575571ae9fe0d3a41bb79424fe9b1"
34 | }
35 | }
36 | }
37 | },
38 | "swiftformat": {
39 | "version": "0.49.13",
40 | "bottle": {
41 | "rebuild": 0,
42 | "root_url": "https://ghcr.io/v2/homebrew/core",
43 | "files": {
44 | "arm64_monterey": {
45 | "cellar": ":any_skip_relocation",
46 | "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:9ca3ab9db1f528cd75201a3cbe8a165c92181fe4353a0aae3002e762992606e2",
47 | "sha256": "9ca3ab9db1f528cd75201a3cbe8a165c92181fe4353a0aae3002e762992606e2"
48 | },
49 | "arm64_big_sur": {
50 | "cellar": ":any_skip_relocation",
51 | "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:7c9ca8d9f5d303a9e4e4e4359022075abb82d835cecab2f374960d80be6f366e",
52 | "sha256": "7c9ca8d9f5d303a9e4e4e4359022075abb82d835cecab2f374960d80be6f366e"
53 | },
54 | "monterey": {
55 | "cellar": ":any_skip_relocation",
56 | "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:6686aa8139c4c0f70c1b6b491ace161e0d767ae3a0d9fb27aa9379968423c93e",
57 | "sha256": "6686aa8139c4c0f70c1b6b491ace161e0d767ae3a0d9fb27aa9379968423c93e"
58 | },
59 | "big_sur": {
60 | "cellar": ":any_skip_relocation",
61 | "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:1d46d7bdd32e2743e54db27aa757c75f454626ddce633fed24c2981c2227617c",
62 | "sha256": "1d46d7bdd32e2743e54db27aa757c75f454626ddce633fed24c2981c2227617c"
63 | },
64 | "catalina": {
65 | "cellar": ":any_skip_relocation",
66 | "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:309a419a4345fd1ef7ad75dddbe0995b5dced750b2621ca3aaa1ba49adaddaed",
67 | "sha256": "309a419a4345fd1ef7ad75dddbe0995b5dced750b2621ca3aaa1ba49adaddaed"
68 | },
69 | "x86_64_linux": {
70 | "cellar": "/home/linuxbrew/.linuxbrew/Cellar",
71 | "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:af6b21401a7697c6f58d9eb84d14f896e017168193f585e3e571a6eddb77fe7b",
72 | "sha256": "af6b21401a7697c6f58d9eb84d14f896e017168193f585e3e571a6eddb77fe7b"
73 | }
74 | }
75 | }
76 | }
77 | }
78 | },
79 | "system": {
80 | "macos": {
81 | "catalina": {
82 | "HOMEBREW_VERSION": "2.4.16",
83 | "HOMEBREW_PREFIX": "/usr/local",
84 | "Homebrew/homebrew-core": "a8b99e19e829d664c643c488031024d07317398d",
85 | "CLT": "11.5.0.0.1.1588476445",
86 | "Xcode": "11.7",
87 | "macOS": "10.15.6"
88 | },
89 | "monterey": {
90 | "HOMEBREW_VERSION": "3.5.6-73-ge217fd3",
91 | "HOMEBREW_PREFIX": "/opt/homebrew",
92 | "Homebrew/homebrew-core": "7813809dc9f6c721c98a0b1f130e9f58aff76450",
93 | "CLT": "13.4.0.0.1.1651278267",
94 | "Xcode": "13.2.1",
95 | "macOS": "12.4"
96 | }
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 3.0
4 |
5 | Murray has been rewritten from scratch!
6 | We tried to maintain compatibility with the previous structure, however some breaking changes may have occured.
7 |
8 | - New configuration file formats now supports YAML and JSON
9 | - MurrayKit completely rewritten in order to be used in graphical applications
10 | - `--preview` option for `murray run` command allows a quick preview about what will be written
11 | - Environment in Murrayfile now supports context resolution
12 | - Dynamic values in contexts now supports current git author, date, time and year and current file path.
13 |
14 |
15 | ## 2.2
16 |
17 | - Add before and after plugin to BonePaths
18 | - Renamed plugin phases to `before` and `after`
19 |
20 | ## 2.1
21 |
22 | - Shell plugin (#37)
23 | - Recursive resolution of parameters in json strings (#39)
24 | - Bugfix: Absolute url support in Murrayfile packages (#40)
25 | - Bugfix: Bone clone support for repositories with BonePackage.json file in root folder (#41)
26 | - Support for plugin execution before and after each procedure (#44)
27 | - XCodePlugin is now based upon tuist/XcodeProj (written in Swift) rather than Cocoapods/Xcodeproj (written in ruby) (#33)
28 | - Fix folder duplication bug (#34)
29 | - SnakeCase filter (#35)
30 | - Got rid of --param explicit command. (#36)
31 |
32 | ## 2.0
33 |
34 | Completely rewritten version of Murray with new JSON and folder structure. See Readme for more informations.
35 |
--------------------------------------------------------------------------------
/Examples/HTML/Murray/HTML/HTML.yml:
--------------------------------------------------------------------------------
1 | name: HTML
2 | description: A package named HTML created from scaffold
3 | procedures:
4 | - name: page
5 | description: An item named page created from scaffold
6 | items:
7 | - page/page.yml
8 |
--------------------------------------------------------------------------------
/Examples/HTML/Murray/HTML/page/Page.html.stencil:
--------------------------------------------------------------------------------
1 |
2 | {{name|firstUppercase}}
3 |
4 |
5 |
12 | Welcome to {{name|firstUppercase}}
13 | This is a very simple detail page
14 |
17 |
--------------------------------------------------------------------------------
/Examples/HTML/Murray/HTML/page/page.yml:
--------------------------------------------------------------------------------
1 | name: page
2 | parameters:
3 | - name: name
4 | isRequired: true
5 | - name: folder
6 | isRequired: false
7 | paths:
8 | - from: Page.html.stencil
9 | to: |
10 | {{ folder|default:"Pages" }}/{{name|firstLowercase}}.html
11 | description: An item named page created from scaffold
12 | replacements:
13 | - destination: Murrayfile.yml
14 | placeholder: "#PagesPlaceholder"
15 | text: |+1
16 | - name: {{name}}
17 | path: {{ folder|default:"Pages"}}/{{name|firstLowercase}}.html
18 | - destination: index.html
19 | placeholder: ""
20 | text: |
21 | {{name|firstUppercase}}
22 |
--------------------------------------------------------------------------------
/Examples/HTML/Murrayfile.yml:
--------------------------------------------------------------------------------
1 | packages:
2 | - Murray/HTML/HTML.yml
3 | environment:
4 | projectName: "{{name}}"
5 | pages:
6 | - name: Home
7 | path: index.html
8 | #PagesPlaceholder
9 | author:
10 | email: stefano.mondino.dev@gmail.com
11 | name: Stefano Mondino
12 |
13 |
14 |
--------------------------------------------------------------------------------
/Examples/HTML/README.md:
--------------------------------------------------------------------------------
1 | # Murray + HTML
2 |
3 | This is a very basic example where Murray can be setup and used to create new HTML pages
--------------------------------------------------------------------------------
/Examples/HTML/Skeleton.yml:
--------------------------------------------------------------------------------
1 | scripts:
2 | - "open index.html"
3 | paths:
4 | - from: index.html
5 | to: index.html
6 | - from: Murrayfile.yml
7 | to: Murrayfile.yml
8 | initializeGit: true
9 |
--------------------------------------------------------------------------------
/Examples/HTML/index.html:
--------------------------------------------------------------------------------
1 |
2 | {{name|firstUppercase}}
3 |
4 |
5 |
9 | Welcome to {{name|firstUppercase}}
10 | This is a very simple html project
11 |
14 |
--------------------------------------------------------------------------------
/Examples/iOS/MurrayDemo/.gitignore:
--------------------------------------------------------------------------------
1 | xcuserdata
--------------------------------------------------------------------------------
/Examples/iOS/MurrayDemo/Makefile:
--------------------------------------------------------------------------------
1 |
2 |
3 | setup:
4 | git init
5 | mint bootstrap
6 |
7 | lint:
8 | mint run swiftlint --fix
9 | mint run swiftformat
--------------------------------------------------------------------------------
/Examples/iOS/MurrayDemo/Mintfile:
--------------------------------------------------------------------------------
1 | realm/Swiftlint@0.51.0
2 | nicklockwood/SwiftFormat@0.51.5
--------------------------------------------------------------------------------
/Examples/iOS/MurrayDemo/Murray/Project/Project.yml:
--------------------------------------------------------------------------------
1 | name: Project
2 | description: A package named Project created from scaffold
3 | procedures:
4 | - name: sceneView
5 | description: Creates a new scene in scene folder
6 | items:
7 | - sceneView/sceneView.yml
8 | - name: sceneViewModel
9 | description: Creates a new scene ViewModel in scene folder
10 | items:
11 | - sceneViewModel/sceneViewModel.yml
12 | - name: scene
13 | description: Creates a new scene with view and viewModel
14 | items:
15 | - sceneViewModel/sceneViewModel.yml
16 | - sceneView/sceneView.yml
17 |
--------------------------------------------------------------------------------
/Examples/iOS/MurrayDemo/Murray/Project/sceneView/SceneView.swift.stencil:
--------------------------------------------------------------------------------
1 | {{fileHeader}}
2 |
3 | import SwiftUI
4 |
5 | struct {{name|firstUppercase}}View: View {
6 | @ObservedObject var viewModel: {{name|firstUppercase}}ViewModel
7 | var body: some View {
8 | VStack {
9 | Text(viewModel.title)
10 | Text("This is the {{name}} screen")
11 | }.tabItem {
12 | Text("{{name|uppercase}}")
13 | }
14 | }
15 | }
16 |
17 | struct {{name|firstUppercase}}View_Previews: PreviewProvider {
18 | static var previews: some View {
19 | {{name|firstUppercase}}View(viewModel: .init())
20 | }
21 | }
--------------------------------------------------------------------------------
/Examples/iOS/MurrayDemo/Murray/Project/sceneView/sceneView.yml:
--------------------------------------------------------------------------------
1 | name: sceneView
2 | parameters:
3 | - name: "name"
4 | isRequired: true
5 | paths:
6 | - from: SceneView.swift.stencil
7 | to: "{{paths.scenes}}/{{name|firstUppercase}}/{{name|firstUppercase}}View.swift"
8 | plugins:
9 | xcode:
10 | targets: ["{{mainTarget}}"]
11 | description: A SwiftUI view with previews
12 | replacements:
13 | - destination: "MurrayDemo/MainTab.swift"
14 | placeholder: "// murray: tab"
15 | text: "{{name|firstUppercase}}View(viewModel: viewModel.{{name|firstLowercase}}ViewModel)\n"
16 |
--------------------------------------------------------------------------------
/Examples/iOS/MurrayDemo/Murray/Project/sceneViewModel/ViewModel.swift.stencil:
--------------------------------------------------------------------------------
1 | {{fileHeader}}
2 |
3 | import Combine
4 | import SwiftUI
5 |
6 | class {{name|firstUppercase}}ViewModel: ObservableObject {
7 | var title: String { "{{name}}".uppercased() }
8 | init() {
9 | }
10 | }
--------------------------------------------------------------------------------
/Examples/iOS/MurrayDemo/Murray/Project/sceneViewModel/ViewModelTests.swift.stencil:
--------------------------------------------------------------------------------
1 | {{fileHeader}}
2 |
3 | import Foundation
4 | import XCTest
5 | @testable import {{mainTarget|firstUppercase}}
6 |
7 | class {{name|firstUppercase}}ViewModelTests: XCTestCase {
8 |
9 | var viewModel: {{name|firstUppercase}}ViewModel = .init()
10 |
11 | override func setUpWithError() throws {
12 | try super.setUpWithError()
13 | viewModel = .init()
14 | }
15 |
16 | func testTitleIsAlwaysUppercased() throws {
17 | XCTAssertEqual(viewModel.title, "{{name|uppercase}}")
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Examples/iOS/MurrayDemo/Murray/Project/sceneViewModel/sceneViewModel.yml:
--------------------------------------------------------------------------------
1 | name: sceneViewModel
2 | parameters: []
3 | paths:
4 | - from: ViewModel.swift.stencil
5 | to: "{{paths.scenes}}/{{name|firstUppercase}}/{{name|firstUppercase}}ViewModel.swift"
6 | plugins:
7 | xcode:
8 | targets: ["{{mainTarget}}"]
9 | - from: ViewModelTests.swift.stencil
10 | to: "{{paths.tests}}/{{name|firstUppercase}}/{{name|firstUppercase}}ViewModel.swift"
11 | plugins:
12 | xcode:
13 | targets: ["{{mainTestTarget}}"]
14 | description: A scene view model with tests
15 | replacements:
16 | - destination: "MurrayDemo/MainTabViewModel.swift"
17 | placeholder: "// murray: viewModel"
18 | text: "let {{name|firstLowercase}}ViewModel = {{name|firstUppercase}}ViewModel()\n"
--------------------------------------------------------------------------------
/Examples/iOS/MurrayDemo/MurrayDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Examples/iOS/MurrayDemo/MurrayDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Examples/iOS/MurrayDemo/MurrayDemo/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Examples/iOS/MurrayDemo/MurrayDemo/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Examples/iOS/MurrayDemo/MurrayDemo/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Examples/iOS/MurrayDemo/MurrayDemo/MainTab.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // MurrayDemo
4 | //
5 | // Created by Stefano Mondino on 28/04/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct MainTab: View {
11 | @ObservedObject var viewModel: MainTabViewModel
12 | var body: some View {
13 | TabView {
14 | // murray: tab
15 | }
16 | }
17 | }
18 |
19 | struct ContentView_Previews: PreviewProvider {
20 | static var previews: some View {
21 | MainTab(viewModel: .init())
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Examples/iOS/MurrayDemo/MurrayDemo/MainTabViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MainTabViewModel.swift
3 | // MurrayDemo
4 | //
5 | // Created by Stefano Mondino on 28/04/23.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 |
11 | class MainTabViewModel: ObservableObject {
12 | // murray: viewModel
13 |
14 | init() {}
15 | }
16 |
--------------------------------------------------------------------------------
/Examples/iOS/MurrayDemo/MurrayDemo/MurrayDemoApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MurrayDemoApp.swift
3 | // MurrayDemo
4 | //
5 | // Created by Stefano Mondino on 28/04/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct MurrayDemoApp: App {
12 | var body: some Scene {
13 | WindowGroup {
14 | MainTab(viewModel: .init())
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Examples/iOS/MurrayDemo/MurrayDemo/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Examples/iOS/MurrayDemo/MurrayDemoTests/MurrayDemoTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MurrayDemoTests.swift
3 | // MurrayDemoTests
4 | //
5 | // Created by Stefano Mondino on 28/04/23.
6 | //
7 |
8 | import XCTest
9 |
10 | final class MurrayDemoTests: XCTestCase {
11 | override func setUpWithError() throws {
12 | // Put setup code here. This method is called before the invocation of each test method in the class.
13 | }
14 |
15 | override func tearDownWithError() throws {
16 | // Put teardown code here. This method is called after the invocation of each test method in the class.
17 | }
18 |
19 | func testExample() throws {
20 | // This is an example of a functional test case.
21 | // Use XCTAssert and related functions to verify your tests produce the correct results.
22 | // Any test you write for XCTest can be annotated as throws and async.
23 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
24 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
25 | }
26 |
27 | func testPerformanceExample() throws {
28 | // This is an example of a performance test case.
29 | measure {
30 | // Put the code you want to measure the time of here.
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Examples/iOS/MurrayDemo/Murrayfile.yml:
--------------------------------------------------------------------------------
1 | packages:
2 | - Murray/Project/Project.yml
3 | environment:
4 | mainTarget: MurrayDemo
5 | mainTestTarget: MurrayDemoTests
6 | fileHeader: |
7 | // {{_author}} - © {{_year}}
8 | paths:
9 | sources: MurrayDemo
10 | tests: MurrayDemoTests
11 | scenes: "{{paths.sources}}/Scenes"
12 | plugins:
13 | shell:
14 | after:
15 | - make lint
16 |
--------------------------------------------------------------------------------
/Examples/iOS/MurrayDemo/README.md:
--------------------------------------------------------------------------------
1 | # Murray Demo - Basic SwiftUI example
2 |
3 | This project contains a super-basic xcode project with an iOS app made with SwiftUI.
4 |
5 | The app only contains an empty `TabView` intended to be used with MVVM pattern (`ObservableObject`)
6 |
7 | Goal: Use Murray CLI to quickly scaffold your empty views.
8 |
9 | ## Pre-requisites
10 | Have 🌱 *[Mint](https://github.com/yonaskolb/mint)* installed on your machine.
11 |
12 | Install Murray with `murray install synesthesia-it/murray`
13 |
14 | Project was tested with Xcode 14.3.
15 |
16 | ## Setup
17 |
18 | Just run
19 | ```console
20 | make setup
21 | ```
22 | the first time you checkout this project.
23 |
24 | SwiftFormat and SwiftLint will be installed and used later on by Murray scripts
25 |
26 | ## Steps
27 |
28 | Open the xcodeproj and run the app. You can also check SwiftUI previews and see if they are working.
29 |
30 | Run the test target as well - it's empty (for now).
31 |
32 | Let's assume you want to add a screen to the tab view, like a `Products` screen with a list of product.
33 |
34 | Murray has been setup (in `Murray` folder + `Murrayfile.yml`) to create a View with a ViewModel that will be automatically included in your main tab. How? run
35 | ```console
36 | murray run scene Products
37 | ```
38 | and double check your xcode project.
39 |
40 | You will find:
41 | - a `ProductsView` containing a super basic SwiftUI view (with preview) tied to a single view model
42 | - a `ProductsViewModel` ObservableObject, exposing (for now) a simple `title` property (uppercased)
43 | - a `ProductsViewModelTests` file in the tests folder, checking that your title is uppercased
44 | - the `MainTabViewModel` will expose a `productsViewModel` variable to it's own view
45 | - the `MainTabView` will include the `ProductsView`
46 | - all the new files added to their respective targets
47 | - the entire project linted with SwiftFormat and SwiftLint (with default set of rules for both of them)
48 |
49 | As bonus, if you did setup your git properly, you should automatically find a file header (first line with comments) containing your git name and current year.
50 |
51 | For more informations, check Murray documentation
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Synesthesia srl
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # Configuration variables:
2 | .DEFAULT_GOAL := build
3 |
4 | # Prepare Application workspace
5 |
6 | build:
7 | swift build -c release
8 | cp -f .build/release/Murray /usr/local/bin/murray
9 |
10 | archive:
11 | swift build -c release
12 | cp -f .build/release/Murray ./murray
13 | chmod +x murray
14 | zip murray.zip murray
15 |
16 | # Install dependencies, download build resources and add pre-commit hook
17 |
18 | lint:
19 | swiftformat ./Sources
20 | swiftlint --fix
21 | swiftlint lint --strict
22 |
23 | git_setup:
24 | eval "$$add_pre_commit_script"
25 |
26 | setup:
27 | brew update && brew bundle
28 | make git_setup
29 |
30 | # Define pre commit script to auto lint and format the code
31 | define _add_pre_commit
32 | if [ -d ".git" ]; then
33 | SWIFTLINT_PATH=`which swiftlint`
34 | SWIFTFORMAT_PATH=`which swiftformat`
35 |
36 | cat > .git/hooks/pre-commit << ENDOFFILE
37 | #!/bin/sh
38 |
39 | FILES=\$(git diff --cached --name-only --diff-filter=ACMR "*.swift" | sed 's| |\\ |g')
40 | [ -z "\$FILES" ] && exit 0
41 |
42 | # Format
43 | ${SWIFTFORMAT_PATH} \$FILES
44 |
45 | # Lint
46 | ${SWIFTLINT_PATH} --fix \$FILES
47 | ${SWIFTLINT_PATH} lint \$FILES
48 |
49 | # Add back the formatted/linted files to staging
50 | echo "\$FILES" | xargs git add
51 |
52 | exit 0
53 | ENDOFFILE
54 |
55 | chmod +x .git/hooks/pre-commit
56 | fi
57 | endef
58 | export add_pre_commit_script = $(value _add_pre_commit)
59 |
60 |
61 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "AEXML",
6 | "repositoryURL": "https://github.com/tadija/AEXML.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "38f7d00b23ecd891e1ee656fa6aeebd6ba04ecc3",
10 | "version": "4.6.1"
11 | }
12 | },
13 | {
14 | "package": "Commander",
15 | "repositoryURL": "https://github.com/kylef/Commander.git",
16 | "state": {
17 | "branch": null,
18 | "revision": "4a1f2fb82fb6cef613c4a25d2e38f702e4d812c2",
19 | "version": "0.9.2"
20 | }
21 | },
22 | {
23 | "package": "Files",
24 | "repositoryURL": "https://github.com/johnsundell/files.git",
25 | "state": {
26 | "branch": null,
27 | "revision": "d273b5b7025d386feef79ef6bad7de762e106eaf",
28 | "version": "4.2.0"
29 | }
30 | },
31 | {
32 | "package": "Komondor",
33 | "repositoryURL": "https://github.com/shibapm/Komondor.git",
34 | "state": {
35 | "branch": null,
36 | "revision": "90b087b1e39069684b1ff4bf915c2aae594f2d60",
37 | "version": "1.1.3"
38 | }
39 | },
40 | {
41 | "package": "PackageConfig",
42 | "repositoryURL": "https://github.com/shibapm/PackageConfig.git",
43 | "state": {
44 | "branch": null,
45 | "revision": "58523193c26fb821ed1720dcd8a21009055c7cdb",
46 | "version": "1.1.3"
47 | }
48 | },
49 | {
50 | "package": "PathKit",
51 | "repositoryURL": "https://github.com/kylef/PathKit.git",
52 | "state": {
53 | "branch": null,
54 | "revision": "3bfd2737b700b9a36565a8c94f4ad2b050a5e574",
55 | "version": "1.0.1"
56 | }
57 | },
58 | {
59 | "package": "Rainbow",
60 | "repositoryURL": "https://github.com/onevcat/Rainbow",
61 | "state": {
62 | "branch": null,
63 | "revision": "626c3d4b6b55354b4af3aa309f998fae9b31a3d9",
64 | "version": "3.2.0"
65 | }
66 | },
67 | {
68 | "package": "ShellOut",
69 | "repositoryURL": "https://github.com/JohnSundell/ShellOut.git",
70 | "state": {
71 | "branch": null,
72 | "revision": "e1577acf2b6e90086d01a6d5e2b8efdaae033568",
73 | "version": "2.3.0"
74 | }
75 | },
76 | {
77 | "package": "Spectre",
78 | "repositoryURL": "https://github.com/kylef/Spectre.git",
79 | "state": {
80 | "branch": null,
81 | "revision": "26cc5e9ae0947092c7139ef7ba612e34646086c7",
82 | "version": "0.10.1"
83 | }
84 | },
85 | {
86 | "package": "Stencil",
87 | "repositoryURL": "https://github.com/stencilproject/Stencil.git",
88 | "state": {
89 | "branch": "master",
90 | "revision": "4f222ac85d673f35df29962fc4c36ccfdaf9da5b",
91 | "version": null
92 | }
93 | },
94 | {
95 | "package": "StencilSwiftKit",
96 | "repositoryURL": "https://github.com/SwiftGen/StencilSwiftKit",
97 | "state": {
98 | "branch": null,
99 | "revision": "20e2de5322c83df005939d9d9300fab130b49f97",
100 | "version": "2.10.1"
101 | }
102 | },
103 | {
104 | "package": "XcodeProj",
105 | "repositoryURL": "https://github.com/tuist/xcodeproj.git",
106 | "state": {
107 | "branch": null,
108 | "revision": "c4d5f9d7f789dd944222be95938810947561e559",
109 | "version": "8.12.0"
110 | }
111 | },
112 | {
113 | "package": "Yams",
114 | "repositoryURL": "https://github.com/jpsim/Yams.git",
115 | "state": {
116 | "branch": null,
117 | "revision": "9ff1cc9327586db4e0c8f46f064b6a82ec1566fa",
118 | "version": "4.0.6"
119 | }
120 | }
121 | ]
122 | },
123 | "version": 1
124 | }
125 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let dependencies: [Target.Dependency] = ["Files", "Rainbow", "Stencil", "StencilSwiftKit", "XcodeProj", "Yams"]
7 |
8 | let package = Package(
9 | name: "Murray",
10 | products: [
11 | .executable(name: "murray", targets: ["Murray"]),
12 | .library(name: "MurrayKit", targets: ["MurrayKit"])
13 | ],
14 | dependencies: [
15 | // Dependencies declare other packages that this package depends on.
16 | // .package(url: /* package url */, from: "1.0.0"),
17 | .package(url: "https://github.com/onevcat/Rainbow", from: "3.0.0"),
18 | .package(
19 | name: "Files",
20 | url: "https://github.com/johnsundell/files.git",
21 | from: "4.0.0"
22 | ),
23 | .package(name: "XcodeProj", url: "https://github.com/tuist/xcodeproj.git", from: "8.0.0"),
24 | .package(url: "https://github.com/kylef/Commander.git", from: "0.8.0"),
25 | .package(url: "https://github.com/stencilproject/Stencil.git", .branch("master")),
26 | .package(url: "https://github.com/jpsim/Yams.git", from: "4.0.3"),
27 | .package(url: "https://github.com/SwiftGen/StencilSwiftKit", from: "2.10.1")
28 |
29 | ],
30 | targets: [
31 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
32 | // Targets can depend on other targets in this package, and on products in packages which this package depends on.
33 | .target(name: "MurrayKit",
34 | dependencies: dependencies),
35 | .target(name: "Murray",
36 | dependencies: ["MurrayKit", "Commander"] + dependencies),
37 | .testTarget(name: "MurrayKitTests",
38 | dependencies: ["MurrayKit"] + dependencies,
39 | resources: [.copy("Mocks")])
40 | ]
41 | )
42 |
--------------------------------------------------------------------------------
/Sources/Murray/Commands/Clone.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Stefano Mondino on 26/07/22.
6 | //
7 |
8 | import Commander
9 | import Foundation
10 | import MurrayKit
11 |
12 | extension Commander.Group {
13 | func cloneCommand(in folder: Folder,
14 | name: String = "clone") {
15 | command(name,
16 | Argument("mainPlaceholder",
17 | description: .runMainPlaceholderDescription),
18 | Argument("path",
19 | description: .cloneGitDescription),
20 | Flag("verbose", description: .verboseDescription),
21 | Flag("copyFromLocalFolder", description: .cloneForceLocalPathDescription),
22 | Argument("subfolder",
23 | description: .cloneGitSubfolderDescription),
24 | Argument<[String]?>("parameters",
25 | description: .runParametersDescription),
26 | description: .cloneDescription) { name, path, verbose, copyFromLocalFolder, subfolder, parameters in
27 |
28 | Clone(path: path,
29 | folder: folder,
30 | subfolderPath: subfolder,
31 | mainPlaceholder: name,
32 | copyFromLocalFolder: copyFromLocalFolder,
33 | parameters: parameters)
34 | .executeAndCatch(verbose: verbose)
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/Murray/Commands/List.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Stefano Mondino on 19/05/22.
6 | //
7 |
8 | import Commander
9 | import Foundation
10 | import MurrayKit
11 |
12 | extension Commander.Group {
13 | func listCommand(in folder: Folder, name: String = "list") {
14 | command(name,
15 | Flag("verbose", description: .verboseDescription),
16 | description: .listDescription) { verbose in
17 | try withVerbose(verbose) {
18 | try List(folder: folder)
19 | .executeAndCatch(verbose: verbose)
20 | }
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/Murray/Commands/Run.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Stefano Mondino on 19/05/22.
6 | //
7 |
8 | import Commander
9 | import Foundation
10 | import MurrayKit
11 |
12 | extension Commander.Group {
13 | func runCommand(in folder: Folder, name: String = "run") {
14 | command(name,
15 | Argument("name",
16 | description: .runNameDescription),
17 | Argument("mainPlaceholder",
18 | description: .runMainPlaceholderDescription),
19 | Flag("verbose", description: .verboseDescription),
20 | Flag("preview", description: .runPreviewDescription),
21 | Argument<[String]?>("parameters",
22 | description: .runParametersDescription),
23 | description: .runDescription) { name, mainPlaceholder, verbose, preview, params in
24 |
25 | Run(folder: folder,
26 | mainPlaceholder: mainPlaceholder,
27 | name: name,
28 | preview: preview,
29 | verbose: verbose,
30 | params: params)
31 | .executeAndCatch(verbose: verbose)
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/Murray/Commands/Scaffold.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Stefano Mondino on 19/05/22.
6 | //
7 |
8 | import Commander
9 | import Foundation
10 | import MurrayKit
11 |
12 | extension Commander.Group {
13 | private func murrayfile(in folder: Folder, name: String = "murrayfile") {
14 | command(name,
15 | Flag("verbose"),
16 | Option("format", default: nil, description: .scaffoldFileFormatDescription),
17 | description: .scaffoldMurrayfileDescription) { verbose, format in
18 | try withVerbose(verbose) {
19 | Scaffold
20 | .murrayfile(encoding: .init(rawValue: format) ?? .yml, in: folder)
21 | .executeAndCatch(verbose: verbose)
22 | }
23 | }
24 | }
25 |
26 | private func skeletonfile(in folder: Folder, name: String = "skeleton") {
27 | command(name,
28 | Flag("verbose"),
29 | Option("format", default: nil, description: .scaffoldFileFormatDescription),
30 | description: .scaffoldSkeletonfileDescription) { verbose, format in
31 | Scaffold
32 | .skeletonfile(encoding: .init(rawValue: format) ?? .yml, in: folder)
33 | .executeAndCatch(verbose: verbose)
34 | }
35 | }
36 |
37 | private func package(in folder: Folder,
38 | name: String = "package") {
39 | command(name,
40 | Flag("verbose"),
41 | Argument("name",
42 | description: .scaffoldPackageNameDescription),
43 |
44 | Option("folder",
45 | default: "Murray",
46 | description: .scaffoldPackageFolderDescription),
47 |
48 | Option("format",
49 | default: nil,
50 | description: .scaffoldFileFormatDescription),
51 | description: .scaffoldPackageDescription) { verbose, name, path, format in
52 | Scaffold
53 | .package(named: name,
54 | encoding: .init(rawValue: format),
55 | description: .init(format: .scaffoldPackageDefaultDescriptionFormat, name),
56 | rootFolder: folder,
57 | path: path)
58 | .executeAndCatch(verbose: verbose)
59 | }
60 | }
61 |
62 | private func item(in folder: Folder,
63 | name: String = "bone") {
64 | command(name,
65 | Flag("verbose"),
66 | Argument("packageName",
67 | description: .scaffoldItemPackageNameDescription),
68 | Argument("name",
69 | description: .scaffoldItemNameDescription),
70 | Flag("skipProcedure",
71 | description: .scaffoldItemSkipProcedureDescription),
72 | Argument<[String]>("files",
73 | description: .scaffoldItemFilesDescription),
74 | description: .scaffoldItemDescription) { verbose, package, name, skipProcedure, files in
75 | Scaffold.item(named: name,
76 | package: package,
77 | description: .init(format: .scaffoldItemDefaultDescriptionFormat, name),
78 | rootFolder: folder,
79 | createProcedure: !skipProcedure,
80 | files: files)
81 | .executeAndCatch(verbose: verbose)
82 | }
83 | }
84 |
85 | private func procedure(in folder: Folder,
86 | name: String = "procedure") {
87 | command(name,
88 | Flag("verbose"),
89 | Argument("packageName",
90 | description: .scaffoldProcedurePackageNameDescription),
91 | Argument("name",
92 | description: .scaffoldProcedureNameDescription),
93 | Argument<[String]>("itemNames",
94 | description: .scaffoldProcedureItemsDescription),
95 | description: .scaffoldProcedureDescription) { verbose, package, name, items in
96 | Scaffold.procedure(named: name,
97 | package: package,
98 | description: .init(format: .scaffoldProcedureDefaultDescriptionFormat, name),
99 | rootFolder: folder,
100 | itemNames: items)
101 | .executeAndCatch(verbose: verbose)
102 | }
103 | }
104 |
105 | func scaffoldCommand(in folder: Folder, name: String = "scaffold") {
106 | group(name) {
107 | $0.murrayfile(in: folder)
108 | $0.skeletonfile(in: folder)
109 | $0.package(in: folder)
110 | $0.item(in: folder)
111 | $0.procedure(in: folder)
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/Sources/Murray/Menu.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Menu.swift
3 | // MurrayCore
4 | //
5 | // Created by Stefano Mondino on 12/07/18.
6 | //
7 |
8 | import Commander
9 |
10 | import Foundation
11 | import MurrayKit
12 | import Rainbow
13 |
14 | func commands() -> Group {
15 | let folder = Folder.current
16 | #if DEBUG
17 | Rainbow.enabled = false
18 | #endif
19 |
20 | return Group { group in
21 | group.listCommand(in: folder)
22 | group.runCommand(in: folder)
23 | group.scaffoldCommand(in: folder)
24 | group.cloneCommand(in: folder)
25 | group.group("bone",
26 | .boneDescription) { group in
27 | group.listCommand(in: folder)
28 | group.runCommand(in: folder, name: "new")
29 | }
30 | // Skeleton.commands(for: $0)
31 | // Bone.commands(for: $0)
32 | // Scaffold.commands(for: $0)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/Murray/Strings.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Stefano Mondino on 19/05/22.
6 | //
7 |
8 | import Foundation
9 | // swiftlint:disable identifier_name line_length
10 | extension String {
11 | @Translation("Print complete log during command execution")
12 | static var verboseDescription: String
13 |
14 | @Translation("A set of commands to interact with bones in current folder")
15 | static var boneDescription: String
16 |
17 | @Translation("List all available bones.")
18 | static var listDescription: String
19 |
20 | @Translation("Run selected procedure in current folder")
21 | static var runDescription: String
22 |
23 | @Translation("Name of the bone from bonespec (example: model). If multiple bonespecs are being used, use . syntax. Example: myBones.model")
24 | static var runNameDescription: String
25 |
26 | @Translation("Value that needs to be replaced in templates wherever the keyword is used.")
27 | static var runMainPlaceholderDescription: String
28 |
29 | @Translation("Previews results instead of actually execute it")
30 | static var runPreviewDescription: String
31 |
32 | @Translation("Custom parameters for templates. Use key:value syntax (ex: \"author:yourname with spaces\")")
33 | static var runParametersDescription: String
34 |
35 | @Translation("Create a new Murrayfile in current folder")
36 | static var scaffoldMurrayfileDescription: String
37 |
38 | @Translation("Create a new Skeleton in current folder")
39 | static var scaffoldSkeletonfileDescription: String
40 |
41 | @Translation("Create a new Package in specified folder")
42 | static var scaffoldPackageDescription: String
43 |
44 | @Translation("Package name")
45 | static var scaffoldPackageNameDescription: String
46 |
47 | @Translation("Default folder containing all Murray packages, relative to Murrayfile directory.")
48 | static var scaffoldPackageFolderDescription: String
49 |
50 | @Translation("A package named %@ created from scaffold")
51 | static var scaffoldPackageDefaultDescriptionFormat: String
52 |
53 | @Translation("An item named %@ created from scaffold")
54 | static var scaffoldItemDefaultDescriptionFormat: String
55 |
56 | @Translation("An procedure named %@ created from scaffold")
57 | static var scaffoldProcedureDefaultDescriptionFormat: String
58 |
59 | @Translation("Create a new item in specified package.")
60 | static var scaffoldItemDescription: String
61 |
62 | @Translation("Name of item to be created")
63 | static var scaffoldItemNameDescription: String
64 | @Translation("Name of package where current item will be included into")
65 | static var scaffoldItemPackageNameDescription: String
66 |
67 | @Translation("File names to be created (empty) and associated to current item")
68 | static var scaffoldItemFilesDescription: String
69 |
70 | @Translation("Skip automatic procedure creation for current item.")
71 | static var scaffoldItemSkipProcedureDescription: String
72 |
73 | @Translation("Create a new procedure in specified package with provided items in sequence.")
74 | static var scaffoldProcedureDescription: String
75 | @Translation("Name of package where procedure will be included into")
76 | static var scaffoldProcedurePackageNameDescription: String
77 | @Translation("Name of the procedure that will be used in run command")
78 | static var scaffoldProcedureNameDescription: String
79 | @Translation("Item names to include in this procedure")
80 | static var scaffoldProcedureItemsDescription: String
81 |
82 | @Translation("File format for file. Can be yml or json. Defaults to yml.")
83 | static var scaffoldFileFormatDescription: String
84 |
85 | @Translation("Clone a remote repository containing a Skeleton file")
86 | static var cloneDescription: String
87 |
88 | @Translation("A path pointing to a valid git repository, either local or remote. To specify a custom branch/tag, use @ or @ or @ right after the url. \nExample: https://github.com/synesthesia-it/Murray@develop")
89 | static var cloneGitDescription: String
90 |
91 | @Translation("Provided path should be intended as a local folder, ignoring git status, copy folder as-is. This is useful for local development to avoid committing every test.")
92 | static var cloneForceLocalPathDescription: String
93 |
94 | @Translation("A subfolder path containing the actual Skeleton project. This is useful when the same repository contains more than one Skeleton,")
95 | static var cloneGitSubfolderDescription: String
96 | }
97 |
98 | @propertyWrapper
99 | struct Translation {
100 | let key: String
101 | init(_ key: String) {
102 | self.key = key
103 | }
104 |
105 | var wrappedValue: String { key }
106 | }
107 |
--------------------------------------------------------------------------------
/Sources/Murray/main.swift:
--------------------------------------------------------------------------------
1 | import Commander
2 | import Foundation
3 | import MurrayKit
4 |
5 | commands().run()
6 |
7 | func withVerbose(_ verbose: Bool, callback: () throws -> Void) rethrows {
8 | if verbose {
9 | Logger.logLevel = .verbose
10 | }
11 | try callback()
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/MurrayKit/Coding/Decoder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Stefano Mondino on 30/01/22.
6 | //
7 |
8 | import Foundation
9 | import Yams
10 |
11 | public protocol Decoder {
12 | func decode(_ data: Data) throws -> Value
13 | }
14 |
15 | public extension Decoder {
16 | func decode(_ data: Data,
17 | of _: Value.Type) throws -> Value {
18 | try decode(data)
19 | }
20 |
21 | func decode(_ string: String,
22 | encoding: String.Encoding = .utf8,
23 | of _: Value.Type) throws -> Value {
24 | let data = string.data(using: encoding) ?? Data()
25 | return try decode(data)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/MurrayKit/Coding/Encoder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Stefano Mondino on 30/01/22.
6 | //
7 |
8 | import Foundation
9 |
10 | public protocol Encoder {
11 | func encode(_ object: Value) throws -> Data
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/MurrayKit/Coding/JSONCoder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Stefano Mondino on 30/01/22.
6 | //
7 |
8 | import Foundation
9 |
10 | extension JSONDecoder: Decoder {
11 | public func decode(_ data: Data) throws -> Value where Value: Decodable {
12 | do {
13 | return try decode(Value.self, from: data)
14 | } catch {
15 | switch error {
16 | case is Errors: throw error
17 | default: throw Errors.unparsableContent(error.localizedDescription)
18 | }
19 | }
20 | }
21 | }
22 |
23 | extension JSONEncoder: Encoder {}
24 |
--------------------------------------------------------------------------------
/Sources/MurrayKit/Coding/Parameters.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Stefano Mondino on 30/01/22.
6 | //
7 |
8 | import Foundation
9 |
10 | public typealias JSON = [String: AnyHashable]
11 |
12 | public struct Parameters: Codable,
13 | Hashable,
14 | Equatable,
15 | ExpressibleByDictionaryLiteral,
16 | ExpressibleByStringLiteral,
17 | ExpressibleByArrayLiteral,
18 | ExpressibleByNilLiteral,
19 | CustomDebugStringConvertible,
20 | CustomStringConvertible,
21 | Collection {
22 | private enum WrappedValue: Codable, Equatable, Hashable {
23 | case `nil`
24 | case string(String)
25 | case array([Parameters])
26 | case dictionary([String: Parameters])
27 | init(_ parameters: Parameters) {
28 | self = parameters.value
29 | }
30 |
31 | var value: AnyHashable? {
32 | switch self {
33 | case let .dictionary(value): return value.reduce(into: JSON()) {
34 | $0[$1.key.description] = $1.value.value.value
35 | }
36 | case let .array(value): return value.compactMap { $0.value.value }
37 | case let .string(value): return value
38 | default: return nil
39 | }
40 | }
41 | }
42 |
43 | public var startIndex: Int { array?.startIndex ?? 0 }
44 |
45 | public var endIndex: Int { array?.endIndex ?? 0 }
46 |
47 | private var value: WrappedValue
48 |
49 | private var array: [Parameters]? {
50 | switch value {
51 | case let .array(value):
52 | return value
53 | default: return nil
54 | }
55 | }
56 |
57 | var dictionaryValue: JSON? {
58 | value.value as? JSON
59 | }
60 |
61 | var arrayValue: [JSON.Value]? {
62 | value.value as? [JSON.Value]
63 | }
64 |
65 | var stringValue: String? {
66 | readValue()
67 | }
68 |
69 | public init(from decoder: Swift.Decoder) throws {
70 | let container = try decoder.singleValueContainer()
71 | if let dictionary = try? container.decode([String: Parameters].self) {
72 | value = .dictionary(dictionary)
73 | } else if let array = try? container.decode([Parameters].self) {
74 | value = .array(array)
75 | } else if let value = try? container.decode(String.self) {
76 | self.value = .string(value)
77 | } else {
78 | value = .nil
79 | }
80 | }
81 |
82 | public func encode(to encoder: Swift.Encoder) throws {
83 | var container = encoder.singleValueContainer()
84 | switch value {
85 | case let .dictionary(value):
86 | try container.encode(value)
87 | case let .array(value):
88 | try container.encode(value)
89 | case let .string(value):
90 | try container.encode(value)
91 | case .nil:
92 | break
93 | }
94 | }
95 |
96 | private init?(_ undefined: Any) {
97 | switch undefined {
98 | case let string as String: self.init(value: .string(string))
99 | case let array as [JSON.Value]: self.init(value: .array(array.compactMap { Parameters($0) }))
100 | case let dictionary as JSON: self.init(dictionary)
101 | default: return nil
102 | }
103 | }
104 |
105 | public init(_ json: JSON) {
106 | value = .dictionary(json.reduce(into: [:]) {
107 | $0[$1.0] = Parameters($1.1)
108 | })
109 | }
110 |
111 | public init(dictionaryLiteral elements: (String, Parameters)...) {
112 | value = .dictionary(elements.reduce(into: [:]) {
113 | $0[$1.0] = $1.1
114 | })
115 | }
116 |
117 | public init(stringLiteral value: StringLiteralType) {
118 | self.value = .string(value)
119 | }
120 |
121 | public init(arrayLiteral elements: Parameters...) {
122 | value = .array(elements)
123 | }
124 |
125 | public init(nilLiteral _: ()) {
126 | value = .nil
127 | }
128 |
129 | private init(value: WrappedValue) {
130 | self.value = value
131 | }
132 |
133 | public var debugDescription: String {
134 | switch value {
135 | case let .dictionary(value):
136 | return value.debugDescription
137 | case let .array(value):
138 | return value.debugDescription
139 | case let .string(value):
140 | return value.debugDescription
141 | case .nil: return String?(nilLiteral: ()).debugDescription
142 | }
143 | }
144 |
145 | public var description: String {
146 | debugDescription
147 | }
148 |
149 | private func readValue() -> Value? {
150 | switch value {
151 | case let .dictionary(value): return value as? Value
152 | case let .array(value): return value as? Value
153 | case let .string(value): return value as? Value
154 | default: return nil
155 | }
156 | }
157 |
158 | public subscript(index: String) -> String? {
159 | self[index]?.readValue()
160 | }
161 |
162 | public subscript(index: String) -> [Parameters]? {
163 | self[index]?.readValue()
164 | }
165 |
166 | public subscript(index: String) -> Parameters? {
167 | get {
168 | switch value {
169 | case let .dictionary(value): return value[index]
170 | default: return nil
171 | }
172 | }
173 | set(newValue) {
174 | switch value {
175 | case let .dictionary(value):
176 | var dictionary = value
177 | dictionary[index] = newValue
178 | self.value = .dictionary(dictionary)
179 | default: break
180 | }
181 | }
182 | }
183 |
184 | public subscript(index: Int) -> Parameters? {
185 | get {
186 | switch value {
187 | case let .array(value): return value[index]
188 | default: return nil
189 | }
190 | }
191 | set(newValue) {
192 | switch value {
193 | case let .array(value):
194 | var array = value
195 | if let newValue = newValue {
196 | array[index] = newValue
197 | } else if array.count > index {
198 | array.remove(at: index)
199 | }
200 | self.value = .array(array)
201 | default: break
202 | }
203 | }
204 | }
205 |
206 | public func index(after index: Int) -> Int {
207 | array?.index(after: index) ?? 0
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/Sources/MurrayKit/Coding/YAMLCoder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Stefano Mondino on 30/01/22.
6 | //
7 |
8 | import Foundation
9 | import Yams
10 |
11 | extension YAMLDecoder: Decoder {
12 | public func decode(_ data: Data) throws -> Value where Value: Decodable {
13 | do {
14 | return try decode(Value.self, from: data)
15 | } catch {
16 | switch error {
17 | case let DecodingError.dataCorrupted(inner):
18 | switch inner.underlyingError ?? error {
19 | case is Errors: throw inner.underlyingError ?? error
20 | default:
21 | if let underlying = inner.underlyingError as? YamlError {
22 | throw Errors.unparsableContent(underlying.description)
23 | }
24 | throw Errors.unparsableContent((inner.underlyingError ?? error).localizedDescription)
25 | }
26 | default: throw Errors.unparsableContent(error.localizedDescription)
27 | }
28 | }
29 | }
30 | }
31 |
32 | extension YAMLEncoder: Encoder {
33 | public func encode(_ object: Value) throws -> Data where Value: Encodable {
34 | let string: String = try encode(object)
35 | return string.data(using: .utf8) ?? Data()
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/MurrayKit/Commands/Clone.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Clone.swift
3 | //
4 | //
5 | // Created by Stefano Mondino on 26/07/22.
6 | //
7 |
8 | import Foundation
9 |
10 | public struct Clone: CommandWithContext {
11 | public let mainPlaceholder: String
12 | // public let repository: Repository
13 | public let path: String
14 | public let forceLocalCopy: Bool
15 | public let params: [String]
16 | public let folder: Folder
17 | public let subfolderPath: String?
18 |
19 | public init(path: String,
20 | folder: Folder,
21 | subfolderPath: String? = nil,
22 | mainPlaceholder: String,
23 | copyFromLocalFolder: Bool = false,
24 | parameters: [String]? = nil) {
25 | self.path = path
26 | self.folder = folder
27 | self.subfolderPath = subfolderPath
28 | self.mainPlaceholder = mainPlaceholder
29 | params = parameters ?? []
30 | forceLocalCopy = copyFromLocalFolder
31 | }
32 |
33 | public func execute() throws {
34 | let context: Template.Context = .init(context(mainPlaceholderKey: "name"))
35 | guard let projectName: String = context.values["name"] as? String else {
36 | throw Errors.unknown
37 | }
38 |
39 | Logger.log("Template context:\n\(context)\n", level: .verbose)
40 |
41 | try? Folder.temporary.subfolder(named: projectName).delete()
42 | var temporaryProjectFolder: Folder
43 | if forceLocalCopy {
44 | Logger.log("Copying contents from: \(path)")
45 | let destination = try Folder.temporary.createSubfolderIfNeeded(withName: projectName)
46 | let copySource = try Folder(path: path)
47 | temporaryProjectFolder = try copySource.copy(to: destination)
48 | } else {
49 | let repository = Repository(at: path)
50 | Logger.log("Cloning repository from \(repository) into \(Folder.temporary.path)",
51 | level: .verbose)
52 | temporaryProjectFolder = try clone(from: repository,
53 | into: Folder.temporary,
54 | projectName: projectName)
55 |
56 | Logger.log("Project cloned to \(temporaryProjectFolder.path)")
57 | }
58 | Logger.log("Creating final project folder at \(folder.path)\(projectName)",
59 | level: .verbose)
60 |
61 | let projectFolder = try folder.createSubfolderIfNeeded(withName: projectName)
62 |
63 | Logger.log("Moving contents from temporary folder")
64 |
65 | if let subfolderPath = subfolderPath {
66 | Logger.log("Looking for subfolder \(subfolderPath) in checked out folder \(temporaryProjectFolder.path)")
67 | try temporaryProjectFolder = temporaryProjectFolder.subfolder(at: subfolderPath)
68 | }
69 |
70 | try temporaryProjectFolder.moveContents(to: projectFolder, includeHidden: true)
71 |
72 | guard let skeleton = try? CodableFile(in: projectFolder) else {
73 | throw Errors.noValidSkeletonFound("\(projectFolder.path)")
74 | }
75 |
76 | Logger.log("Deleting original git folder, if present",
77 | level: .verbose)
78 | try? projectFolder.subfolder(named: ".git").delete()
79 |
80 | try resolvePaths(for: skeleton,
81 | projectFolder: projectFolder,
82 | context: context)
83 |
84 | Logger.log("Launching custom scripts",
85 | level: .verbose)
86 | try skeleton.object.scripts.forEach {
87 | let script = try $0.resolve(with: context)
88 | Logger.log("\(script)",
89 | level: .verbose)
90 | try Process().launchBash(with: script, in: projectFolder)
91 | }
92 |
93 | if skeleton.object.initializeGit {
94 | Logger.log("Initializing git version control",
95 | level: .verbose)
96 | try Process().launchBash(with: "git init", in: projectFolder)
97 | }
98 |
99 | Logger.log("Deleting skeleton file at \(skeleton.file.path(relativeTo: folder))",
100 | level: .verbose)
101 | try skeleton.file.delete()
102 | Logger.log("Created new project named \(projectName) at \(projectFolder.path)")
103 | }
104 |
105 | private func resolvePaths(for skeleton: CodableFile,
106 | projectFolder: Folder,
107 | context: Template.Context) throws {
108 | Logger.log("Resolving paths",
109 | level: .verbose)
110 | let pluginManager = PluginManager.shared
111 | try skeleton.object.paths.forEach { path in
112 | let enrichedContext = context.adding(path.customParameters())
113 | try pluginManager.execute(.init(element: path,
114 | context: enrichedContext,
115 | phase: .before,
116 | root: projectFolder))
117 |
118 | try skeleton.writeableFiles(for: path,
119 | resolveSource: false,
120 | context: context,
121 | destinationRoot: projectFolder)
122 | .forEach { file in
123 | try file.commit(context: context)
124 | }
125 | try pluginManager.execute(.init(element: path,
126 | context: enrichedContext,
127 | phase: .after,
128 | root: projectFolder))
129 | }
130 | Logger.log("Deleting original paths",
131 | level: .verbose)
132 | skeleton.object.paths
133 | .filter { $0.from != ((try? $0.to.resolve(with: context)) ?? $0.to) }
134 | .map { $0.from }
135 | .forEach {
136 | Logger.log("Deleting \($0)",
137 | level: .verbose)
138 | try? projectFolder.file(at: $0).delete()
139 | try? projectFolder.subfolder(at: $0).delete()
140 | }
141 | }
142 |
143 | private func clone(from repository: Repository,
144 | into folder: Folder,
145 | projectName: String) throws -> Folder {
146 | do {
147 | var command = "git clone --single-branch --depth 1 "
148 | if repository.version.isEmpty == false {
149 | command += "--branch \(repository.version) "
150 | }
151 |
152 | command += repository.repo + " " + folder.path + projectName
153 | Logger.log("Cloning - command: \(command)", level: .verbose)
154 | try Process().launchBash(with: command)
155 | let projectFolder = try folder.subfolder(named: projectName)
156 | return projectFolder
157 | } catch {
158 | Logger.log("\(error)", level: .verbose)
159 | switch error {
160 | case is Errors: throw error
161 | default: throw Errors.invalidGitRepository(repository.package)
162 | }
163 | }
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/Sources/MurrayKit/Commands/Command.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Stefano Mondino on 19/05/22.
6 | //
7 |
8 | import Foundation
9 |
10 | public protocol Command {
11 | func execute() throws
12 | func executeAndCatch(verbose: Bool)
13 | }
14 |
15 | public protocol CommandWithContext: Command {
16 | var mainPlaceholder: String { get }
17 | var params: [String] { get }
18 | }
19 |
20 | public extension CommandWithContext {
21 | func context(mainPlaceholderKey: String) -> JSON {
22 | return params.reduce(into: [mainPlaceholderKey: mainPlaceholder]) { context, pair in
23 | let elements = pair.components(separatedBy: ":")
24 | guard elements.count == 2 else { return }
25 | context[elements[0]] = elements[1]
26 | }
27 | }
28 | }
29 |
30 | public extension Command {
31 | func executeAndCatch(verbose: Bool) {
32 | do {
33 | if verbose {
34 | Logger.logLevel = .verbose
35 | }
36 | try execute()
37 | } catch {
38 | switch error {
39 | case let error as Errors: Logger.log(.error(error))
40 | // case let error as Files.LocationError:
41 | // Logger.log("Some error occured with file at:\(error.path)")
42 | default: Logger.log(error.localizedDescription)
43 | }
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/MurrayKit/Commands/List.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Stefano Mondino on 30/01/22.
6 | //
7 |
8 | import Foundation
9 |
10 | public struct List {
11 | public let murrayfile: CodableFile
12 |
13 | public init(murrayfile: CodableFile) {
14 | self.murrayfile = murrayfile
15 | }
16 |
17 | public init(folder: Folder,
18 | murrayfileName: String = Murrayfile.defaultName)
19 | throws {
20 | do {
21 | let murrayfile = try CodableFile(in: folder, defaultName: murrayfileName)
22 | self.init(murrayfile: murrayfile)
23 | } catch {
24 | switch error {
25 | case Errors.fileLocationError: throw Errors.murrayfileNotFound(folder.path)
26 | case is Errors: throw error
27 | default: throw Errors.murrayfileNotFound(folder.path)
28 | }
29 | }
30 | }
31 |
32 | public func packages() throws -> [CodableFile] {
33 | try murrayfile.packages()
34 | }
35 |
36 | public func list() throws -> [PackagedProcedure] {
37 | try murrayfile.packages()
38 | .flatMap { package in
39 | package.object
40 | .procedures
41 | .map { .init(package: package,
42 | procedure: $0) }
43 | }
44 | }
45 | }
46 |
47 | extension List: Command {
48 | public func execute() throws {
49 | let list = try list()
50 | let strings = list.map {
51 | "\($0.package.object.name.lightGreen).\($0.procedure.name.green): \($0.procedure.description)"
52 | }
53 | strings.forEach {
54 | Logger.log($0, level: .normal)
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Sources/MurrayKit/Commands/Run.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Run.swift
3 | //
4 | //
5 | // Created by Stefano Mondino on 19/05/22.
6 | //
7 |
8 | import Foundation
9 |
10 | public struct Run: CommandWithContext {
11 | var folder: Folder
12 | public let mainPlaceholder: String
13 | let name: String
14 | let preview: Bool
15 | public let params: [String]
16 | let verbose: Bool
17 |
18 | public init(folder: Folder,
19 | mainPlaceholder: String,
20 | name: String,
21 | preview: Bool,
22 | verbose: Bool,
23 | params: [String]?) {
24 | self.folder = folder
25 | self.mainPlaceholder = mainPlaceholder
26 | self.name = name
27 | self.params = params ?? []
28 | self.preview = preview
29 | self.verbose = verbose
30 | }
31 |
32 | public func execute() throws {
33 | let murrayfile = try CodableFile(in: folder)
34 | let context = context(mainPlaceholderKey: murrayfile.object.namePlaceholder)
35 |
36 | let pipeline = try Pipeline(murrayfile: murrayfile,
37 | procedure: name,
38 | context: .init(context))
39 |
40 | let missingParameters = try pipeline.missingParameters()
41 | guard missingParameters.isEmpty else {
42 | throw Errors
43 | .missingRequiredParameters(missingParameters)
44 | }
45 |
46 | let invalidParameters = try pipeline.invalidParameters()
47 | guard invalidParameters.isEmpty else {
48 | throw Errors
49 | .invalidParameters(invalidParameters)
50 | }
51 |
52 | let files = try pipeline.writeableFiles()
53 | if preview {
54 | try files.forEach { file in
55 | let contents = try file.preview(context: pipeline.context)
56 | if verbose {
57 | Logger.log("File contents preview:\n\n\(contents)", level: .normal)
58 | }
59 | }
60 | } else {
61 | Logger.log("Running pipeline '\(name)'")
62 | try pipeline.run()
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Sources/MurrayKit/Commands/Scaffold.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Stefano Mondino on 20/05/22.
6 | //
7 |
8 | import Foundation
9 | import Yams
10 |
11 | public struct Scaffold: Command {
12 | private let closure: () throws -> Void
13 |
14 | private init(_ closure: @escaping () throws -> Void) {
15 | self.closure = closure
16 | }
17 |
18 | public static func murrayfile(named name: String = Murrayfile.defaultName,
19 | encoding: CodableFile.Encoding,
20 | in folder: Folder) -> Scaffold {
21 | .init {
22 | let file = Murrayfile.empty
23 | try CodableFile.create(file,
24 | encoding: encoding,
25 | named: "\(name).\(encoding.rawValue)",
26 | in: folder)
27 | }
28 | }
29 |
30 | public static func skeletonfile(named name: String = Skeleton.defaultName,
31 | encoding: CodableFile.Encoding,
32 | in folder: Folder) -> Scaffold {
33 | .init {
34 | let file = Skeleton.empty
35 | try CodableFile.create(file,
36 | encoding: encoding,
37 | named: "\(name).\(encoding.rawValue)",
38 | in: folder)
39 | }
40 | }
41 |
42 | public static func package(named name: String,
43 | encoding: CodableFile.Encoding? = nil,
44 | description: String,
45 | rootFolder: Folder,
46 | path: String = "Murray") -> Scaffold {
47 | return Scaffold {
48 | var murrayfile = try CodableFile(in: rootFolder)
49 |
50 | let encoding = encoding ?? murrayfile.encoding()
51 |
52 | let package = Package(name: name,
53 | description: description,
54 | procedures: [])
55 | let packageName = "\(name).\(encoding.rawValue)"
56 |
57 | let packageStructure = try CodableFile.create(package,
58 | encoding: encoding,
59 | named: packageName,
60 | in: rootFolder.createSubfolderIfNeeded(withName: path).createSubfolderIfNeeded(withName: name))
61 |
62 | let newPath = packageStructure.file.path(relativeTo: rootFolder)
63 |
64 | try murrayfile.update {
65 | $0.add(packagePath: newPath)
66 | }
67 | }
68 | }
69 |
70 | public static func item(named name: String,
71 | package packageName: String,
72 | encoding: CodableFile- .Encoding? = nil,
73 | description: String,
74 | rootFolder: Folder,
75 | createProcedure: Bool = true,
76 | files: [String]) -> Scaffold {
77 | Scaffold {
78 | let murrayfile = try CodableFile(in: rootFolder)
79 | guard var package = try murrayfile
80 | .packages()
81 | .first(where: { $0.object.name == packageName }),
82 | let packageFolder = package.file.parent
83 | else {
84 | throw Errors.invalidPackageName(packageName)
85 | }
86 |
87 | let targetFolder = try packageFolder.createSubfolderIfNeeded(withName: name)
88 | let encoding = encoding ?? murrayfile.encoding()
89 |
90 | let paths = try files.map {
91 | try targetFolder
92 | .createFileIfNeeded(at: $0)
93 | .path(relativeTo: targetFolder)
94 | }.map { Item.Path(from: $0, to: "") }
95 |
96 | let item = MurrayKit.Item(name: name,
97 | parameters: [],
98 | paths: paths,
99 | plugins: nil,
100 | optionalDescription: description,
101 | replacements: [])
102 | let fileName = "\(name).\(encoding.rawValue)"
103 |
104 | guard (try? targetFolder.file(named: fileName)) == nil else {
105 | throw Errors.itemAlreadyExists(name)
106 | }
107 |
108 | let file = try CodableFile.create(item,
109 | encoding: encoding,
110 | named: fileName,
111 | in: targetFolder)
112 |
113 | if createProcedure {
114 | let itemRelativePath = file.file.path(relativeTo: packageFolder)
115 | let procedure = Procedure(name: name,
116 | description: description,
117 | plugins: nil,
118 | itemPaths: [itemRelativePath])
119 | try package.update {
120 | $0.add(procedure: procedure)
121 | }
122 | }
123 | }
124 | }
125 |
126 | public static func procedure(named name: String,
127 | package packageName: String,
128 | description: String,
129 | rootFolder: Folder,
130 | itemNames: [String]) -> Scaffold {
131 | Scaffold {
132 | let murrayfile = try CodableFile(in: rootFolder)
133 | guard var package = try murrayfile
134 | .packages()
135 | .first(where: { $0.object.name == packageName }),
136 | let packageFolder = package.file.parent
137 | else {
138 | throw Errors.invalidPackageName(packageName)
139 | }
140 |
141 | guard package.object
142 | .procedures
143 | .first(where: { $0.name.lowercased() == name.lowercased() }) == nil
144 | else {
145 | throw Errors.procedureAlreadyExists(name)
146 | }
147 |
148 | let availableItems = try Set(package.items())
149 | let itemPaths = try itemNames.map { itemName -> String in
150 | guard let item = availableItems.first(where: { $0.object.name == itemName }) else {
151 | throw Errors.itemNotFound(itemName)
152 | }
153 | return item.file.path(relativeTo: packageFolder)
154 | }
155 |
156 | let procedure = Procedure(name: name,
157 | description: description,
158 | plugins: nil,
159 | itemPaths: itemPaths)
160 |
161 | try package.update {
162 | $0.add(procedure: procedure)
163 | }
164 | }
165 | }
166 |
167 | public func execute() throws {
168 | try closure()
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/Sources/MurrayKit/Errors.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Errors.swift
3 | //
4 | //
5 | // Created by Stefano Mondino on 30/01/22.
6 | //
7 |
8 | import Foundation
9 |
10 | public enum Errors: Swift.Error, Equatable, Hashable {
11 | public static func == (lhs: Errors, rhs: Errors) -> Bool {
12 | lhs.localizedDescription == rhs.localizedDescription
13 | }
14 |
15 | case unparsableFile(String)
16 | case unparsableContent(String)
17 | case unresolvableString(string: String, context: JSON)
18 | case invalidReplacement
19 | case unknown
20 | case folderLocationError(String)
21 | case fileLocationError(String)
22 | case unreadableFile(String)
23 | case unwriteableFile(String)
24 | case copyFolder(String)
25 | case moveFolder(String)
26 | case createFolder(String)
27 | case deleteFolder(String)
28 | case deleteFile(String)
29 | case procedureNotFound(name: String)
30 | case multipleProceduresFound(name: String)
31 | case murrayfileNotFound(String)
32 | case invalidPackageName(String)
33 | case itemAlreadyExists(String)
34 | case itemNotFound(String)
35 | case procedureAlreadyExists(String)
36 | case noValidSkeletonFound(String)
37 | case invalidGitRepository(String)
38 | case missingRequiredParameters([Item.Parameter])
39 | case invalidParameters([Item.Parameter])
40 | }
41 |
42 | private extension Array where Element == Item.Parameter {
43 | func invalidDescription() -> String {
44 | map {
45 | [$0.name,
46 | $0.description != $0.name ? $0.description : nil,
47 | "Allowed values: \(($0.values ?? []).joined(separator: ", "))"]
48 | .compactMap { $0 }
49 | .joined(separator: " - ")
50 | }
51 | .joined(separator: "\n")
52 | }
53 |
54 | func missingDescription() -> String {
55 | map {
56 | if $0.name == $0.description { return $0.name }
57 | return [$0.name,
58 | $0.description]
59 | .joined(separator: " - ")
60 | }
61 | .joined(separator: "\n")
62 | }
63 | }
64 |
65 | extension Errors: LocalizedError, CustomStringConvertible {
66 | public var description: String { localizedDescription.red }
67 | var localizedDescription: String {
68 | switch self {
69 | case let .missingRequiredParameters(parameters):
70 | return "Missing required parameters: \(parameters.missingDescription())"
71 | case let .invalidParameters(parameters):
72 | return "Provided parameters are not valid:\n\(parameters.invalidDescription())"
73 | case let .unparsableFile(filePath): return "Path at \(filePath) is not parsable"
74 | case let .unparsableContent(error): return "Unparsable content: \(error)"
75 | case let .unresolvableString(string, context):
76 | return "Provided string is not properly resolvable\n\nString:\n\(string)\n\nContext:\n\n\(context)"
77 | case .invalidReplacement: return "Error during replacement"
78 | case let .procedureNotFound(name): return "Procedure '\(name)' not found."
79 | case let .multipleProceduresFound(name): return "Multiple procedures found for name \(name). Use syntax PackageName.procedureName instead."
80 | case let .folderLocationError(path): return "Invalid folder at \(path)"
81 | case let .fileLocationError(path): return "Invalid file at \(path)"
82 | case let .unreadableFile(path): return "Unreadable file at \(path)"
83 | case let .unwriteableFile(path): return "Unwriteable file at \(path)"
84 | case let .copyFolder(path): return "Error copying folder at \(path)"
85 | case let .moveFolder(path): return "Error moving folder to \(path)"
86 | case let .createFolder(path): return "Error creating folder at \(path)"
87 | case let .deleteFolder(path): return "Error deleting folder at \(path)"
88 | case let .deleteFile(path): return "Error deleting file at \(path)"
89 | case let .murrayfileNotFound(path): return "No valid Murrayfile found in \(path)"
90 | case let .invalidPackageName(name): return "Provided package name '\(name)' is invalid. Check your Murrayfile."
91 | case let .itemAlreadyExists(name): return "Item named'\(name)' already exists."
92 | case let .itemNotFound(name): return "Item named '\(name)' not found"
93 | case let .procedureAlreadyExists(name): return "Procedure named '\(name)' already exists"
94 | case let .noValidSkeletonFound(path): return "No valid skeleton file found at \(path)"
95 | case let .invalidGitRepository(path): return "Invalid git repository at \(path)"
96 | case .unknown: return "Some error occurred"
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/Sources/MurrayKit/Logger/Logger.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Logger.swift
3 | // App
4 | //
5 | // Created by Stefano Mondino on 17/07/18.
6 | // Copyright © 2018 Synesthesia. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Rainbow
11 |
12 | public enum LogLevel: Int {
13 | case verbose = 1
14 | case warning = 10
15 | case network = 20
16 | case error = 100
17 | case normal = 200
18 | case none = 1000
19 |
20 | public func colorize(string: String) -> String {
21 | switch self {
22 | case .verbose: return string
23 | case .warning: return string.yellow
24 | case .network: return string.blue
25 | case .error: return string.red
26 | default: return string
27 | }
28 | }
29 |
30 | public var symbol: String {
31 | switch self {
32 | case .verbose: return "💬"
33 | case .network: return "🌐"
34 | case .warning: return "⚠️"
35 | case .error: return "⛔️"
36 | default: return ""
37 | }
38 | }
39 | }
40 |
41 | public protocol LoggerType: AnyObject {
42 | var logLevel: LogLevel { get set }
43 | func log(_ log: Log, level: LogLevel, tag: String?)
44 | }
45 |
46 | public enum Log: CustomStringConvertible, CustomDebugStringConvertible, Equatable {
47 | case error(Errors)
48 | case message(String)
49 |
50 | public var debugDescription: String {
51 | switch self {
52 | case let .error(error): return error.localizedDescription
53 | case let .message(string): return string
54 | }
55 | }
56 |
57 | public var description: String {
58 | switch self {
59 | case let .error(error): return error.localizedDescription
60 | case let .message(string): return string
61 | }
62 | }
63 | }
64 |
65 | open class ConsoleLogger: LoggerType {
66 | public var logLevel: LogLevel
67 |
68 | public init(logLevel: LogLevel) {
69 | self.logLevel = logLevel
70 | }
71 |
72 | open func string(_ message: String, level: LogLevel, tag: String?) -> String? {
73 | if logLevel.rawValue > level.rawValue { return nil }
74 |
75 | let string =
76 | """
77 | \([
78 | [level.symbol, tag]
79 | .compactMap { $0 }
80 | .filter { !$0.isEmpty }
81 | .joined(separator: " "),
82 | level
83 | .colorize(string: message)
84 | ]
85 | .compactMap { $0 }
86 | .filter { !$0.isEmpty }
87 | .joined(separator: ": ")
88 | )
89 | """
90 | return string
91 | }
92 |
93 | open func log(_ log: Log, level: LogLevel, tag: String?) {
94 | guard let string = string(log.description, level: level, tag: tag) else { return }
95 | print(string)
96 | }
97 | }
98 |
99 | public final class TestLogger: ConsoleLogger {
100 | public var messages: [Log] = []
101 |
102 | public var lastMessage: Log? { messages.last }
103 |
104 | override public func log(_ log: Log, level: LogLevel, tag _: String?) {
105 | // let message = log.description
106 | // guard let string = string(message, level: level, tag: tag) else { return }
107 | // print(string)
108 | if logLevel.rawValue > level.rawValue { return }
109 | messages.append(log)
110 | print(log.description)
111 | }
112 | }
113 |
114 | public enum Logger {
115 | public static var logger: LoggerType = ConsoleLogger(logLevel: .normal)
116 | public static var logLevel: LogLevel {
117 | get { logger.logLevel }
118 | set { logger.logLevel = newValue }
119 | }
120 |
121 | public static func log(_ message: String, level: LogLevel = .normal, tag: String? = nil) {
122 | log(.message(message), level: level, tag: tag)
123 | }
124 |
125 | public static func log(_ log: Log, level: LogLevel = .normal, tag: String? = nil) {
126 | logger.log(log, level: level, tag: tag)
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/Sources/MurrayKit/Models/CodableFile.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Stefano Mondino on 30/01/22.
6 | //
7 |
8 | import Foundation
9 | import Yams
10 |
11 | /// A codable object with a local file system (File) representation
12 | public struct CodableFile: Hashable {
13 | public enum Encoding: String {
14 | case json
15 | case yml
16 |
17 | static var allValidExtensions: [String] {
18 | ["json", "yml", "yaml"]
19 | }
20 |
21 | fileprivate var encoder: Encoder {
22 | switch self {
23 | case .json: return JSONEncoder()
24 | case .yml: return YAMLEncoder()
25 | }
26 | }
27 |
28 | public init?(rawValue: String?) {
29 | guard let rawValue = rawValue else {
30 | return nil
31 | }
32 | switch rawValue {
33 | case "json": self = .json
34 | case "yml", "yaml": self = .yml
35 | default: return nil
36 | }
37 | }
38 | }
39 |
40 | public let file: File
41 | public private(set) var object: Object
42 |
43 | public init(file: File, object: Object) {
44 | self.file = file
45 | self.object = object
46 | }
47 |
48 | public mutating func reload() throws {
49 | object = try Self(file: file).object
50 | }
51 |
52 | public init(file: File,
53 | type _: Object.Type = Object.self) throws {
54 | // self.file = file
55 | let data = try file.read()
56 | if let ext = file.extension,
57 | let decoder: Decoder = Self.decoder(from: ext) {
58 | do {
59 | try self.init(file: file, object: decoder.decode(data))
60 | } catch {
61 | Logger.log("\(error)", level: .error)
62 | throw Errors.unparsableFile(file.path)
63 | }
64 |
65 | } else {
66 | let decoders: [Decoder] = [JSONDecoder(), YAMLDecoder()]
67 | guard let object = decoders
68 | .lazy
69 | .compactMap({ try? $0.decode(data, of: Object.self) })
70 | .first
71 | else {
72 | throw Errors.unparsableFile(file.path)
73 | }
74 | self.init(file: file, object: object)
75 | }
76 | }
77 |
78 | fileprivate static func decoder(from ext: String) -> Decoder? {
79 | switch ext.lowercased() {
80 | case "json":
81 | return JSONDecoder()
82 | case "yml", "yaml":
83 | return YAMLDecoder(encoding: .utf8)
84 | default:
85 | return nil
86 | }
87 | }
88 |
89 | fileprivate static func encoder(from ext: String) -> Encoder? {
90 | switch ext.lowercased() {
91 | case "json":
92 | return JSONEncoder()
93 | case "yml", "yaml":
94 | return YAMLEncoder()
95 | default:
96 | return nil
97 | }
98 | }
99 |
100 | @discardableResult
101 | public static func create(_ object: Object,
102 | encoding: Encoding = .yml,
103 | named name: String,
104 | in folder: Folder) throws -> CodableFile