Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parser for list of parameters #8

Merged
merged 14 commits into from
Feb 15, 2017
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
* Refactor stencil node tests to not use templates and output files.
[David Jennes](https://github.com/djbe)
[#17](https://github.com/SwiftGen/StencilSwiftKit/issues/17)
* Add a "parameters parser" able to transform parameters passed as a set of strings (`a=1 b.x=2 b.y=3 c=4 c=5`) — typically provided as the command line arguments of a CLI — into a Dictionary suitable for Stencil contexts.
[David Jennes](https://github.com/djbe)
[#8](https://github.com/SwiftGen/StencilSwiftKit/pull/8)

## Before 5.0.0

Expand Down
2 changes: 1 addition & 1 deletion Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ CHECKOUT OPTIONS:
SPEC CHECKSUMS:
PathKit: f8260c3e41bf4d552f3603853e32a5b325a176d4
Stencil: 4177c0cabcdc40dd9be9b7f701d710d0a121023a
StencilSwiftKit: e06089cf69771405f2afabe48df97ed9ac159593
StencilSwiftKit: e2caece14b65d144da6bbe513a73b01be4833e7b

PODFILE CHECKSUM: 254c61439b3ceadef162f42c05a5d1c157f1a6df

Expand Down
2 changes: 1 addition & 1 deletion Pods/Local Podspecs/StencilSwiftKit.podspec.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Pods/Manifest.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion Pods/Pods.xcodeproj/project.pbxproj

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,33 @@ This template subclass aims to remove those lines generated by using a simple wo
## Stencil.Extension & swiftStencilEnvironment

This framework also contains [helper methods for `Stencil.Extension` and `Stencil.Environment`](https://github.com/SwiftGen/StencilSwiftKit/blob/master/Sources/Environment.swift), to easily register all the tags and filters listed above on an existing `Stencil.Extension`, as well as to easily get a `Stencil.Environment` preconfigured with both those tags & filters `Extension` and the `StencilSwiftTemplate`.

## Parameters

This framework contains an additional parser, meant to parse a list of parameters from the CLI. For example, using [Commander](https://github.com/kylef/Commander), if you receive a `[String]` from a `VariadicOption<String>`, you can use the parser to convert it into a structured dictionary. For example:

```swift
["foo=1", "bar=2", "baz.qux=hello", "baz.items=a", "baz.items=b", "something"]
```

will become

```swift
[
"foo": "1",
"bar": "2",
"baz": [
"qux": "hello",
"items": [
"a",
"b"
]
],
something: true
]
```

For easier use, you can use the `enrich(context:parameters:)` function to add the following variables to a context:

- `param`: the parsed parameters using the parser mentioned above.
- `env`: a dictionary with all available environment variables (such as `PATH`).
18 changes: 18 additions & 0 deletions Sources/Context.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// Context.swift
// Pods
//
// Created by David Jennes on 14/02/2017.
//
//

import Foundation

public func enrich(context: [AnyHashable: Any], parameters: [String]) throws -> [AnyHashable: Any] {
var context = context

context["env"] = ProcessInfo().environment
context["param"] = try Parameters.parse(items: parameters)

return context
}
80 changes: 80 additions & 0 deletions Sources/Parameters.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//
// StencilSwiftKit
// Copyright (c) 2017 SwiftGen
// MIT Licence
//

import Foundation

public enum ParametersError: Error {
case invalidSyntax(value: Any)
case invalidKey(key: String, value: Any)
case invalidStructure(key: String, oldValue: Any, newValue: Any)
}

public enum Parameters {
typealias Parameter = (key: String, value: Any)
public typealias StringDict = [String: Any]

public static func parse(items: [String]) throws -> StringDict {
let parameters: [Parameter] = try items.map { item in
let parts = item.components(separatedBy: "=")
if parts.count >= 2 {
return (key: parts[0], value: parts.dropFirst().joined(separator: "="))
} else if let part = parts.first, parts.count == 1 && validate(key: part) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does that mean that foo.bar.baz isn't a valid boolean key? (as validate(key: "foo.bar.baz") will return false)?

return (key: part, value: true)
} else {
throw ParametersError.invalidSyntax(value: item)
}
}

return try parameters.reduce(StringDict()) {
try parse(parameter: $1, result: $0)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parameters.reduce(StringDict()) { try Parse(item: $1, résultats: $0) }?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parameters.reduce(StringDict()) { try Parse(item: $1, résultats: $0) }?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That french autocorrect 😆

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😂

}

private static func parse(parameter: Parameter, result: StringDict) throws -> StringDict {
let parts = parameter.key.components(separatedBy: ".")
let key = parts.first ?? ""
var result = result

// validate key
guard validate(key: key) else { throw ParametersError.invalidKey(key: parameter.key, value: parameter.value) }

// no sub keys, may need to convert to array if repeat key if possible
if parts.count == 1 {
if let current = result[key] as? [Any] {
result[key] = current + [parameter.value]
} else if let current = result[key] as? String {
result[key] = [current, parameter.value]
} else if let current = result[key] {
throw ParametersError.invalidStructure(key: key, oldValue: current, newValue: parameter.value)
} else {
result[key] = parameter.value
}
} else if parts.count > 1 {
guard result[key] is StringDict || result[key] == nil else {
throw ParametersError.invalidStructure(key: key, oldValue: result[key] ?? "", newValue: parameter.value)
}

// recurse into sub keys
let current = result[key] as? StringDict ?? StringDict()
let sub = (key: parts.suffix(from: 1).joined(separator: "."), value: parameter.value)
result[key] = try parse(parameter: sub, result: current)
}

return result
}

// a valid key is not empty and only alphanumerical or dot
private static func validate(key: String) -> Bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this means 1 is a valid key too?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now, yeah. Shouldn't matter that much, say I want to create a (fake) tuple.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. Fine by be

return !key.isEmpty &&
key.rangeOfCharacter(from: notAlfanumericsAndDot) == nil
}

private static let notAlfanumericsAndDot: CharacterSet = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/alfa/alpha 😄

var result = CharacterSet.alphanumerics
result.insert(".")
return result.inverted
}()
}
8 changes: 8 additions & 0 deletions StencilSwiftKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
DD5F34301E21A3A200AEB5DA /* StringFiltersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5F342C1E21A3A200AEB5DA /* StringFiltersTests.swift */; };
DD5F34311E21A3A200AEB5DA /* SwiftIdentifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5F342D1E21A3A200AEB5DA /* SwiftIdentifierTests.swift */; };
DDE1E2F61E3E33E30043367C /* MacroNodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE1E2F51E3E33E30043367C /* MacroNodeTests.swift */; };
DDE1E2F91E3FABE70043367C /* ParametersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE1E2F81E3FABE70043367C /* ParametersTests.swift */; };
DDFD1F691E5358C70023AE2B /* ContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFD1F681E5358C70023AE2B /* ContextTests.swift */; };
/* End PBXBuildFile section */

/* Begin PBXBuildRule section */
Expand Down Expand Up @@ -61,6 +63,8 @@
DD5F342C1E21A3A200AEB5DA /* StringFiltersTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringFiltersTests.swift; sourceTree = "<group>"; };
DD5F342D1E21A3A200AEB5DA /* SwiftIdentifierTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftIdentifierTests.swift; sourceTree = "<group>"; };
DDE1E2F51E3E33E30043367C /* MacroNodeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MacroNodeTests.swift; sourceTree = "<group>"; };
DDE1E2F81E3FABE70043367C /* ParametersTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParametersTests.swift; sourceTree = "<group>"; };
DDFD1F681E5358C70023AE2B /* ContextTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextTests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -133,7 +137,9 @@
isa = PBXGroup;
children = (
DD5F342A1E21A3A200AEB5DA /* CallNodeTests.swift */,
DDFD1F681E5358C70023AE2B /* ContextTests.swift */,
DDE1E2F51E3E33E30043367C /* MacroNodeTests.swift */,
DDE1E2F81E3FABE70043367C /* ParametersTests.swift */,
DD5F342B1E21A3A200AEB5DA /* SetNodeTests.swift */,
DD5F342C1E21A3A200AEB5DA /* StringFiltersTests.swift */,
DD5F342D1E21A3A200AEB5DA /* SwiftIdentifierTests.swift */,
Expand Down Expand Up @@ -271,7 +277,9 @@
DDE1E2F61E3E33E30043367C /* MacroNodeTests.swift in Sources */,
DD5F341B1E21993A00AEB5DA /* TestsHelper.swift in Sources */,
DD5F34301E21A3A200AEB5DA /* StringFiltersTests.swift in Sources */,
DDFD1F691E5358C70023AE2B /* ContextTests.swift in Sources */,
DD5F342E1E21A3A200AEB5DA /* CallNodeTests.swift in Sources */,
DDE1E2F91E3FABE70043367C /* ParametersTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
16 changes: 16 additions & 0 deletions StencilSwiftKit.xcodeproj/xcshareddata/xcschemes/Tests.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "NO"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F46217047B43043671E1C8A5BE689A1F"
BuildableName = "StencilSwiftKit.framework"
BlueprintName = "StencilSwiftKit"
ReferencedContainer = "container:Pods/Pods.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
Expand Down
51 changes: 51 additions & 0 deletions Tests/StencilSwiftKitTests/ContextTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// ContextTests.swift
// StencilSwiftKit
//
// Created by David Jennes on 14/02/2017.
// Copyright © 2017 AliSoftware. All rights reserved.
//

import XCTest
import StencilSwiftKit

class ContextTests: XCTestCase {
func testEmpty() {
let context = [String: Any]()

let result = try! enrich(context: context, parameters: [])
XCTAssertEqual(result.count, 2, "2 items have been added")

guard let env = result["env"] as? [AnyHashable: Any] else { XCTFail("`env` should be a dictionary"); return }
XCTAssertNotNil(env["PATH"] as? String)
guard let params = result["param"] as? [AnyHashable: Any] else { XCTFail("`param` should be a dictionary"); return }
XCTAssertEqual(params.count, 0)
}

func testWithContext() {
let context: [String : Any] = ["foo": "bar", "hello": true]

let result = try! enrich(context: context, parameters: [])
XCTAssertEqual(result.count, 4, "4 items have been added")
XCTAssertEqual(result["foo"] as? String, "bar")
XCTAssertEqual(result["hello"] as? Bool, true)

guard let env = result["env"] as? [AnyHashable: Any] else { XCTFail("`env` should be a dictionary"); return }
XCTAssertNotNil(env["PATH"] as? String)
guard let params = result["param"] as? [AnyHashable: Any] else { XCTFail("`param` should be a dictionary"); return }
XCTAssertEqual(params.count, 0)
}

func testWithParameters() {
let context = [String: Any]()

let result = try! enrich(context: context, parameters: ["foo=bar", "hello"])
XCTAssertEqual(result.count, 2, "2 items have been added")

guard let env = result["env"] as? [AnyHashable: Any] else { XCTFail("`env` should be a dictionary"); return }
XCTAssertNotNil(env["PATH"] as? String)
guard let params = result["param"] as? [AnyHashable: Any] else { XCTFail("`param` should be a dictionary"); return }
XCTAssertEqual(params["foo"] as? String, "bar")
XCTAssertEqual(params["hello"] as? Bool, true)
}
}
Loading