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

Add env render module #43

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions packages/pkl.env/PklProject
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//===----------------------------------------------------------------------===//
// Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
amends "../basePklProject.pkl"

package {
version = "1.0.0"
}
4 changes: 4 additions & 0 deletions packages/pkl.env/PklProject.deps.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"schemaVersion": 1,
"resolvedDependencies": {}
}
123 changes: 123 additions & 0 deletions packages/pkl.env/env.pkl
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//===----------------------------------------------------------------------===//
// Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
/// A renderer for [ENV](https://github.com/motdotla/dotenv) configuration files.
///
/// Basic usage:
/// ```
/// import "package://pkg.pkl-lang.org/pkl-pantry/[email protected]#/env.pkl"
///
/// output {
/// renderer = new env.Renderer {}
/// }
/// ```
@ModuleInfo { minPklVersion = "0.25.0" }
module pkl.env.env

/// Renders values as ENV.
class Renderer extends ValueRenderer {
/// Value converters to apply before values are rendered.
///
/// For further information see [PcfRenderer.converters].
/// For path converters, only "*" is supported.
converters: Mapping<Class|String, (unknown) -> Any>

function renderDocument(value: Any): String =
renderValue(value)

function renderValue(value: Any): String =
let (_value = convert(value, false))
render(_value, List()).trim()

local function convert(value: Any, skipConversion: Boolean): unknown =
if (!skipConversion && !getConverters(value).isEmpty)
convert(applyConverters(value), true)
else if (isTrueDynamic(value))
throw("""
Cannot render object with both properties/entries and elements as ENV. Received: \(value).
""")
else if (isMapLike(value))
getMap(value).mapValues((_, elem) -> convert(elem, false))
else if (isListLike(value))
getList(value).map((elem) -> convert(elem, false))
else value

local function applyConverters(value: Any): unknown =
let (converters = getConverters(value))
converters.fold(value, (acc, converter) -> converter.apply(acc))

local function render(value: Any, prefix: List<String>): String =
if (value is Map)
renderMap(value, prefix)
else if (value is List)
renderList(value, prefix)
else
renderInline(value)

local function renderMap(map: Map, prefix: List<String>): String =
map.fold("", (acc, key, val) -> renderLine(acc, prefix.add(key), val))

local function renderList(list: List, prefix: List<String>): String =
list.foldIndexed("", (i, acc, elem) -> renderLine(acc, prefix.add(i.toString()), elem))

local function renderLine(acc: String, idPath: List<String>, val: Any): String =
acc + if (val is List|Map)
render(val, idPath) else
makeAssignmentExpression(idPath, val)
+ "\n"

local function makeAssignmentExpression(idPath: List<String>, value: Any): String =
"\(makeId(idPath))=\(renderInline(value))"

local function makeId(idPath: List<String>): String =
idPath
.map((component: String) -> component.split(" "))
.flatten()
.filter((component: String) -> !component.isEmpty)
.map((component: String) -> component.toUpperCase())
.join("_")

local function renderInline(value: Any): String =
if (value is String && value.contains("\""))
"`\(value.split("\n").join("\\n"))`"
else if (value is String)
jsonRenderer.renderValue(value)
else
"\"" + value.toString() + "\""

local function getConverters(value: Any): List<(Any) -> unknown> =
new Listing {
when (convertersMap.containsKey(value.getClass())) {
convertersMap[value.getClass()]
}
when (convertersMap.containsKey("*")) {
convertersMap["*"]
}
}.toList()

local jsonRenderer = new JsonRenderer {}

local convertersMap = converters.toMap()

local function getMap(obj: unknown): Map = (if (obj is Map) obj else obj.toMap())

local function getList(obj: unknown): List = (if (obj is List) obj else obj.toList())

local function isTrueDynamic(obj: Any): Boolean = (obj is Dynamic && !obj.toList().isEmpty && !obj.toMap().isEmpty)

local function isMapLike(obj: Any): Boolean = (obj is Dynamic && obj.toList().isEmpty) || obj is (Typed|Map|Mapping)

local function isListLike(obj: Any): Boolean = (obj is Dynamic && !obj.toList().isEmpty) || obj is (List|Listing)
}
134 changes: 134 additions & 0 deletions packages/pkl.env/examples/basic.pkl
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
//===----------------------------------------------------------------------===//
// Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
module pkl.env.examples.basic

import "../env.pkl"

title = "ENV Example"

owner {
name = "Tom Preston-Werner"
organization = "Github"
bio = """
GitHub Cofounder & CEO
Likes tater tots and beer
"""
}

database {
server = "192.168.1.1"
ports {
8001
8001
8002
}
connection_max = 5000
enabled = true
}

servers {
alpha {
ip = "10.0.0.1"
dc = "eqdc10"
}
beta {
ip = "10.0.0.2"
dc = "eqdc10"
country = "中国"
}
}

clients {
data {
new {
"gamma"
"delta"
}
new {
1
2
}
}
hosts {
"alpha"
"omega"
}
}


products {
new {
name = "Hammer Bro"
sku = 738594937
}
new {}
new {
name = "Nail"
sku = 284758393
color = "gray"
`1-1` = "〇😀"
}
}

fruits {
new {
name = "apple"
physical {
color = "red"
shape = "round"
}
varieties {
new { name = "red delicious" }
new { name = "granny smith" }
}
}
new {
name = "banana"
varieties {
new { name = "plantain" }
}
}
}

contributors {
"Foo Bar <[email protected]>"
new {
name = "Baz Qux"
email = "[email protected]"
url = "https://example.com/bazqux"
}
}

dog {
`tater.man` {
type {
name = "pug"
}
age = NaN
maxAge = Infinity
}
}

cat {
["favourite crossword"] {
title = "Daily Word"
paper = null
}
}

output {
renderer = new env.Renderer {}
}
49 changes: 49 additions & 0 deletions packages/pkl.env/examples/converters.pkl
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//===----------------------------------------------------------------------===//
// Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
module pkl.env.examples.converters

import "../env.pkl"

class Dog {
breed: String
sleepTime: Duration
}

dogs {
new Dog {
breed = "Golden Retreiver"
sleepTime = 12.h
}
new Dog {
breed = "GERMAN SHEPHERD"
sleepTime = 10.h
}
new Dog {
breed = "greyhound"
sleepTime = 18.h
}
}

output {
renderer = new env.Renderer {
converters {
[Dog] = (dog: Dog) -> (dog) {
breed = dog.breed.toLowerCase()
}
[Duration] = (dur: Duration) -> "\(dur.value)\(dur.unit)"
}
}
}
50 changes: 50 additions & 0 deletions packages/pkl.env/examples/quotes.pkl
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//===----------------------------------------------------------------------===//
// Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import "../env.pkl"

multiLine = """
The quick red fox
jumps over the lazy brown
dog.
"""

multiLineList {
"""
Beauty will
save the
"""
"""
world.
"""
}

embeddedDoubles = "\"What do you think?\" shouted Razumihin."

embeddedSingles = "'What do you think?' shouted Razumihin."

embeddedMixed = "\"What do you think?\" 'shouted' Razumihin."

mutliLineEmbeddedMixed = """
\"Beauty\" will
save the
'world'.
"""

oddSpacing = " this look kinda off."

output {
renderer = new env.Renderer {}
}
Loading