Skip to content

Commit

Permalink
Merge pull request #5 from twof/directoryAssociations
Browse files Browse the repository at this point in the history
Associations work with entire directories
  • Loading branch information
twof committed Dec 27, 2021
2 parents a7e1fa9 + f4b1e7d commit dbf47c7
Show file tree
Hide file tree
Showing 10 changed files with 139 additions and 136 deletions.
17 changes: 4 additions & 13 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -1,31 +1,22 @@
{
"object": {
"pins": [
{
"package": "Files",
"repositoryURL": "https://github.com/JohnSundell/Files",
"state": {
"branch": null,
"revision": "22fe84797d499ffca911ccd896b34efaf06a50b9",
"version": "4.1.1"
}
},
{
"package": "swift-argument-parser",
"repositoryURL": "https://github.com/apple/swift-argument-parser",
"state": {
"branch": null,
"revision": "92646c0cdbaca076c8d3d0207891785b3379cbff",
"version": "0.3.1"
"revision": "e1465042f195f374b94f915ba8ca49de24300a0d",
"version": "1.0.2"
}
},
{
"package": "Yams",
"repositoryURL": "https://github.com/jpsim/Yams.git",
"state": {
"branch": null,
"revision": "88caa2e6fffdbef2e91c2022d038576062042907",
"version": "4.0.0"
"revision": "9ff1cc9327586db4e0c8f46f064b6a82ec1566fa",
"version": "4.0.6"
}
}
]
Expand Down
21 changes: 11 additions & 10 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.
// swift-tools-version:5.5

import PackageDescription

let package = Package(
name: "downstream",
products: [
.executable(name: "downstream", targets: ["downstream"])
],
dependencies: [
.package(url: "https://github.com/jpsim/Yams.git", from: "4.0.0"),
.package(url: "https://github.com/JohnSundell/Files", from: "4.0.0"),
.package(url: "https://github.com/apple/swift-argument-parser", from: "0.3.0"),
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.2"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
.executableTarget(
name: "downstream",
dependencies: [
"Yams",
"Files",
.product(name: "Yams", package: "Yams"),
.product(name: "ArgumentParser", package: "swift-argument-parser")
],
exclude: [
Expand All @@ -26,6 +24,9 @@ let package = Package(
),
.testTarget(
name: "DownstreamTests",
dependencies: ["downstream"]),
dependencies: [
.target(name: "downstream")
]
),
]
)
33 changes: 25 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
# Downstream

A tool to alert users when files they're changing may cause docs to be out of date. Downstream is more or less a reverse dependency manager in that it's used to describe what relies on your code rather than what your code relies on.
A tool to alert users when files they're changing may cause docs to be out of date. Downstream is more or less a
reverse dependency manager in that it's used to describe what relies on your code rather than what your code relies on.

## Why?

There's a pretty consistent problem accross orgs I've been in where people are hesitent to write docs, guides, etc because they're concerned that what they write will rapidly become out of date. This fear is legitimate, and out of date docs are a common problem. Letting people know what docs need to be updated upon file changes is a step towards solving this problem.
There's a pretty consistent problem accross orgs I've been in where people are hesitent to write docs, guides, etc
because they're concerned that what they write will rapidly become out of date. This fear is legitimate, and out of
date docs are a common problem. Letting people know what docs need to be updated upon file changes is a step towards
solving this problem.

## Installation

Expand Down Expand Up @@ -57,37 +61,46 @@ jobs:
body: ${{ steps.get_changes.outputs.content }}
```

For some background, `file_changes` records a list of files that have been changed in this PR to `steps.file_changes.outputs.files`. It's basically the equivalent of `git diff --name-only`.
For some background, `file_changes` records a list of files that have been changed in this PR to
`steps.file_changes.outputs.files`. It's basically the equivalent of `git diff --name-only`.

These lines
```
content="${content//'%'/'%25'}"
content="${content//$'\n'/'%0A'}"
content="${content//$'\r'/'%0D'}"
```
are necessary due to [a bug in Github Actions](https://github.community/t/set-output-truncates-multiline-strings/16852) that prevents multiple lines from being passed to `set-output`.
are necessary due to [a bug in Github Actions](https://github.community/t/set-output-truncates-multiline-strings/16852)
that prevents multiple lines from being passed to `set-output`.

### Project Structure

You will need to put a file called `downstream.yml` in the directory with the file you'd like to attach documentation to.
You will need to put a file called `downstream.yml` in the directory with the file you'd like to attach documentation
to.
```
Sources/Downstream/
├── Associations.swift
├── downstream.yml
└── main.swift
```

`downstream.yml` will need to contain a `[String: [String]]` dictionary where the keys are file names in that directory and values are links/paths/wherever users can find documentation that relies on that file.
`downstream.yml` will need to contain a `[String: [String]]` dictionary where the keys are file names in that directory
and values are links/paths/wherever users can find documentation that relies on that file. Anything under a "\*" will
work as an asociation for the entire directory.

```yaml
associations:
main.swift:
- https://github.com/JohnSundell/Files/blob/master/Sources/Files.swift
- https://github.com/twof/Downstream/edit/main/README.md
Associations.swift:
- https://github.com/twof/Downstream/edit/main/README.md
*:
- https://docs.downstream.io/usage
```

The hook is only capable of failing if a `downstream.yml` is invalid. Otherwise it only exists to provide information. Given the above example, if `Associations.swift` was changed, output would look like this
The hook is only capable of failing if a `downstream.yml` is invalid. Otherwise it only exists to provide information.
Given the above example, if `Associations.swift` was changed, output would look like this

```
$ git commit -am "bumped pre-commit hook"
Expand All @@ -104,7 +117,11 @@ https://github.com/twof/Downstream/blob/main/README.md

### Usage

Beyond its usage as a pre-commit hook, Downstream can also be executed manually for integration with CI and whatnot like can be seen above with Github Actions. It can currently produce output based on the format requested by the user with the `-o` flag. Possible options are `human` for human friendly output like seen in the example above, `yaml`, `json`, and `list` which simply lists out all of the docs that may need updates in a format that's convenient for intake in a bash script.
Beyond its usage as a pre-commit hook, Downstream can also be executed manually for integration with CI and whatnot like
can be seen above with Github Actions. It can currently produce output based on the format requested by the user with
the `-o` flag. Possible options are `human` for human friendly output like seen in the example above, `yaml`, `json`,
and `list` which simply lists out all of the docs that may need updates in a format that's convenient for intake in a
bash script.

```
$ downstream -h
Expand Down
10 changes: 0 additions & 10 deletions Sources/downstream/Associations.swift

This file was deleted.

3 changes: 3 additions & 0 deletions Sources/downstream/AssociationsFile.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
struct AssociationsFile: Codable {
let associations: [String: [String]]
}
80 changes: 49 additions & 31 deletions Sources/downstream/DownstreamArgument.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
//
// File.swift
//
//
// Created by Alex Reilly on 10/24/20.
//

import ArgumentParser
import Foundation
import Files
import Yams

enum OutputFormat: String, ExpressibleByArgument {
Expand All @@ -17,47 +9,73 @@ enum OutputFormat: String, ExpressibleByArgument {
case humanFriendly = "human"
}

typealias TodoList = [String: [String]]

extension TodoList {
func matches(_ filename: String) -> [String] {
let results = [self["*"], self[filename]].compactMap { $0 }.flatMap { $0 }
return results
}
}

struct DownstreamArgument: ParsableCommand {
@Option(name: .shortAndLong, help: "The format of the output")
var outputFormat: OutputFormat?
var outputFormat: OutputFormat = .humanFriendly

@Argument(help: "Input files")
var files: [String]
var files: [String] = []

mutating func run() throws {
let todoList = todos(fileList: self.files)
let todoList = try todos(fileList: self.files)
let output = outputFactory(todos: todoList, format: outputFormat)

print(output)
}

func todos(fileList: [String]) -> [String: [String]] {

// Associations file is used in place of an actual file during tests
func todos(fileList: [String], associationsFiles: [String: AssociationsFile]?=nil) throws -> TodoList {
return try fileList.reduce(into: TodoList()) { (result, filePath) in
let parent = URL(fileURLWithPath: filePath).deletingLastPathComponent().path
let matches = try matches(forFile: filePath, associationsFile: associationsFiles?[parent])
if !matches.isEmpty {
result[filePath] = matches
}
}
}

func matches(forFile path: String, associationsFile: AssociationsFile?=nil) throws -> [String] {
let decoder = YAMLDecoder()

return fileList.reduce(into: [String: [String]]()) { (result, filePath) in
let changedFile = try! File(path: filePath)


if let associationsFile = associationsFile {
let changedFile = URL(fileURLWithPath: path)
return associationsFile.associations.matches(changedFile.lastPathComponent)
} else {
// This path should be coming from git diff, so we expect it to be valid
let changedFile = URL(fileURLWithPath: path)
let parent = changedFile.deletingLastPathComponent()
let downstreamYML = parent.appendingPathComponent("downstream.yml")

if
let parent = changedFile.parent,
let downsteamYML = try? parent.file(named: "downstream.yml").read()
let contentData = FileManager.default.contents(atPath: downstreamYML.path),
let associations = String(data: contentData, encoding: .utf8)
{
guard let associationsFile = try? decoder.decode(AssociationsFile.self, from: downsteamYML) else {
print("\(parent.path)downstream.yml could not be parsed")
DownstreamArgument.exit()
guard let associationsFile = try? decoder.decode(AssociationsFile.self, from: associations) else {
throw ValidationError("\(parent.path)downstream.yml could not be parsed")
}
let fileName = changedFile.name
let newTodos = associationsFile.associations[fileName]
result[filePath] = newTodos
let fileName = changedFile.lastPathComponent
return associationsFile.associations.matches(fileName)
}
}

return []
}

/// Formats the todo list in the selected format type. Defaults to .humanFriendly if no format type is provided.
/// - Parameters:
/// - todos: Todo list in the form of changed file -> associated tasks
/// - format: How the list ought to be formatted. Defaults to .humanFriendly if no format type is provided
/// - Returns: Todo list formatted as desired.
func outputFactory(todos: [String: [String]], format: OutputFormat?) -> String {
func outputFactory(todos: TodoList, format: OutputFormat?) -> String {
let format = format ?? .humanFriendly

switch format {
Expand All @@ -72,13 +90,13 @@ struct DownstreamArgument: ParsableCommand {
}
}

func humanReadableOutput(todos: [String: [String]]) -> String {
func humanReadableOutput(todos: TodoList) -> String {
return todos.map { (filePath, todos) in
return "Due to changes made to \(filePath), you may need to make updates to the following: \n \(todos.joined(separator: "\n"))"
}.joined(separator: "\n")
}

func jsonOutput(todos: [String: [String]]) -> String {
func jsonOutput(todos: TodoList) -> String {
let encoder = JSONEncoder()
guard
let jsonData = try? encoder.encode(todos),
Expand All @@ -91,7 +109,7 @@ struct DownstreamArgument: ParsableCommand {
return jsonString
}

func yamlOutput(todos: [String: [String]]) -> String {
func yamlOutput(todos: TodoList) -> String {
let encoder = YAMLEncoder()
guard
let yamlString = try? encoder.encode(todos)
Expand All @@ -103,7 +121,7 @@ struct DownstreamArgument: ParsableCommand {
return yamlString
}

func listOutput(todos: [String: [String]]) -> String {
func listOutput(todos: TodoList) -> String {
return Set(todos.values).map { $0.joined(separator: "\n") }.joined(separator: "\n")
}
}
6 changes: 0 additions & 6 deletions Sources/downstream/main.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
//
// main.swift
// Downstream
//
// Created by Alex Reilly on 10/22/20.
//
import Foundation

DownstreamArgument.main()
Expand Down

0 comments on commit dbf47c7

Please sign in to comment.