Skip to content

Commit

Permalink
Towards Auto API
Browse files Browse the repository at this point in the history
- add Workspace
  • Loading branch information
pawelprazak committed Dec 20, 2023
1 parent 2c8bb73 commit b0395f0
Show file tree
Hide file tree
Showing 8 changed files with 920 additions and 4 deletions.
18 changes: 18 additions & 0 deletions auto/project.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//> using scala 3.3.1
//> using options -java-output-version:11 -encoding:utf-8
//> using options -deprecation -feature -Werror -Wunused:all

//> using dep org.virtuslab::besom-json:0.1.1-SNAPSHOT
//> using dep org.virtuslab::besom-core:0.1.1-SNAPSHOT
//> using dep org.virtuslab::scala-yaml:0.0.8

//> using test.dep "org.scalameta::munit:1.0.0-M10"

//> using publish.name "besom-auto"
//> using publish.organization "org.virtuslab"
//> using publish.url "https://github.com/VirtusLab/besom"
//> using publish.vcs "github:VirtusLab/besom"
//> using publish.license "Apache-2.0"
//> using publish.repository "central"
//> using publish.developer "lbialy|Łukasz Biały|https://github.com/lbialy"
//> using publish.developer "pawelprazak|Paweł Prażak|https://github.com/pawelprazak"
13 changes: 13 additions & 0 deletions auto/src/main/scala/besom/auto/Stack.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package besom.auto

/** Stack is an isolated, independently configurable instance of a Pulumi program. Stack exposes methods for the full pulumi lifecycle
* (up/preview/refresh/destroy), as well as managing configuration. Multiple Stacks are commonly used to denote different phases of
* development (such as development, staging and production) or feature branches (such as feature-x-dev, jane-feature-x-dev).
*
* @param workspace
* the workspace associated with the stack
* @param stackName
* the name of the stack
*/
case class Stack(workspace: Workspace, stackName: String)

562 changes: 562 additions & 0 deletions auto/src/main/scala/besom/auto/Workspace.scala

Large diffs are not rendered by default.

123 changes: 123 additions & 0 deletions auto/src/main/scala/besom/model/Names.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package besom.model

import scala.compiletime.*
import scala.compiletime.ops.string.*
import scala.language.implicitConversions

/** Name is an identifier. */
opaque type Name <: String = String
object Name:
private val NameFirstCharRegexpPattern = "[A-Za-z0-9_.-]"
private val NameRestCharRegexpPattern = "[A-Za-z0-9_.-]*"
private[model] val NameRegexpPattern = NameFirstCharRegexpPattern + NameRestCharRegexpPattern

private val NameRegexp = NameRegexpPattern.r
private[model] val NameFirstCharRegexp = ("^" + NameFirstCharRegexpPattern + "$").r
private[model] val NameRestCharRegexp = ("^" + NameRestCharRegexpPattern + "$").r

/** IsName checks whether a string is a legal Name. */
def isName(s: String): Boolean = s.nonEmpty && NameRegexp.findFirstIn(s).isDefined

/** Parse a string into a [[Name]].
* @param s
* is a string to parse
* @return
* a [[Name]] if the string is valid, otherwise a compile time error occurs
*/
inline def apply(s: String): Name =
requireConst(s)
inline if !constValue[Matches[s.type, "[A-Za-z0-9_.-][A-Za-z0-9_.-]*"]] then
error("Invalid Name string. Must match '[A-Za-z0-9_.-][A-Za-z0-9_.-]*'.")
else s

implicit inline def str2Name(inline s: String): Name = Name(s)

private[besom] def unsafeOf(s: String): Name = s

extension (name: Name)
/** Turns a [[Name]] into a qualified name, this is legal, since Name's is a proper subset of QName's grammar.
* @return
* the [[Name]] as a [[QName]]
*/
def asQName: QName = QName.unsafeOf(name)

end Name

/** QName is a qualified identifier. The "/" character optionally delimits different pieces of the name. Each element conforms to Name
* regexp pattern. For example, "pulumi/pulumi/stack".
*/

opaque type QName <: String = String
object QName:
/** Parse a string into a [[QName]].
* @param s
* is a string to parse
* @return
* a [[QName]] if the string is valid, otherwise a compile time error occurs
*/
inline def apply(s: String): QName =
requireConst(s)
inline if !constValue[Matches[s.type, "([A-Za-z0-9_.-][A-Za-z0-9_.-]*/)*[A-Za-z0-9_.-][A-Za-z0-9_.-]*"]] then
error("Invalid QName string. Must match '([A-Za-z0-9_.-][A-Za-z0-9_.-]*/)*[A-Za-z0-9_.-][A-Za-z0-9_.-]*'.")
else s

implicit inline def str2QName(inline s: String): QName = QName(s)

private[besom] def unsafeOf(s: String): QName = s

/** QNameDelimiter is what delimits Namespace and Name parts. */
private val QNameDelimiter = "/"
private val QNameRegexpPattern = "(" + Name.NameRegexpPattern + "\\" + QNameDelimiter + ")*" + Name.NameRegexpPattern
private val QNameRegexp = QNameRegexpPattern.r

/** IsQName checks whether a string is a legal QName. */
def isQName(s: String): Boolean = s.nonEmpty && QNameRegexp.findFirstIn(s).isDefined

/** Converts an arbitrary string into a [[QName]], converting the string to a valid [[QName]] if necessary. The conversion is
* deterministic, but also lossy.
*/
def convert(s: String): QName =
val output = s.split(QNameDelimiter).filter(_.nonEmpty).map { segment =>
val chars = segment.toCharArray
if (!Name.NameFirstCharRegexp.matches(chars.head.toString)) chars.update(0, '_')
for (i <- 1 until chars.length) {
if (!Name.NameRestCharRegexp.matches(chars(i).toString)) chars.update(i, '_')
}
new String(chars)
}
val result = output.mkString(QNameDelimiter)
if (result.isEmpty) QName.unsafeOf("_") else QName.unsafeOf(result)

end convert

extension (qname: QName)
/** Extracts the [[Name]] portion of a [[QName]] (dropping any namespace). */
def name: Name =
val ix = qname.lastIndexOf(QNameDelimiter)
val nmn = if ix == -1 then qname else qname.substring(ix + 1)
assert(Name.isName(nmn), s"QName $qname has invalid name $nmn")
nmn

/** Extracts the namespace portion of a [[QName]] (dropping the name), this may be empty. */
def namespace: QName =
val ix = qname.lastIndexOf(QNameDelimiter)
val qn = if ix == -1 then "" else qname.substring(0, ix)
assert(isQName(qn), s"QName $qname has invalid namespace $qn")
QName.unsafeOf(qn)

end QName

/** PackageName is a qualified name referring to an imported package. */
type PackageName = QName

/** ModuleName is a qualified name referring to an imported module from a package. */
type ModuleName = QName

/** ModuleMemberName is a simple name representing the module member's identifier. */
type ModuleMemberName = Name

/** ClassMemberName is a simple name representing the class member's identifier. */
type ClassMemberName = Name

/** TypeName is a simple name representing the type's name, without any package/module qualifiers. */
type TypeName = Name
129 changes: 129 additions & 0 deletions auto/src/test/scala/besom/model/NamesTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package besom.model

import besom.test.CompileAssertions

class NamesTest extends munit.FunSuite with CompileAssertions:

import QName.* // force use of QName extensions instead of TestOptionsConversions.name

test("validation - all alpha"):
compiles("""import besom.model._
val n = Name("simple")
""")

test("validation - mixed-case alpha"):
compiles("""import besom.model._
val n = Name("SiMplE")
""")

test("validation - alphanumeric"):
compiles("""import besom.model._
val n = Name("simple0")
""")

test("validation - mixed-case alphanumeric"):
compiles("""import besom.model._
val n = Name("SiMpLe0")
""")

test("validation - permit underscore"):
compiles("""import besom.model._
val n = Name("_")
""")

test("validation - mixed-case alphanumeric/underscore"):
compiles("""import besom.model._
val n = Name("s1MPl3_")
""")
compiles("""import besom.model._
val n = Name("_s1MPl3")
""")

test("validation - permit hyphens"):
compiles("""import besom.model._
val n = Name("hy-phy")
""")

test("validation - start with ."):
compiles("""import besom.model._
val n = Name(".dotstart")
""")

test("validation - start with -"):
compiles("""import besom.model._
val n = Name("-hyphenstart")
""")

test("validation - start with numbers"):
compiles("""import besom.model._
val n = Name("0num")
""")

test("validation - start with numbers"):
compiles("""import besom.model._
val n = Name("9num")
""")

test("validation - multi-part name"):
compiles("""import besom.model._
val n = QName("namespace/complex")
""")
failsToCompile("""import besom.model._
val n = Name("namespace/complex")
""")

test("validation - multi-part, alphanumeric, etc. name"):
compiles("""import besom.model._
val n = QName("_naMeSpace0/coMpl3x32")
""")
failsToCompile("""import besom.model._
val n = Name("_naMeSpace0/coMpl3x32")
""")

test("validation - even more complex parts"):
compiles("""import besom.model._
val n = QName("n_ameSpace3/moRenam3sp4ce/_Complex5")
""")
failsToCompile("""import besom.model._
val n = Name("n_ameSpace3/moRenam3sp4ce/_Complex5")
""")

test("validation - bad characters"):
failsToCompile("""import besom.model._
val n = QName("s!mple")
""")
failsToCompile("""import besom.model._
val n = Name("s!mple")
""")
failsToCompile("""import besom.model._
val n = QName("namesp@ce/complex")
""")
failsToCompile("""import besom.model._
val n = Name("namesp@ce/complex")
""")
failsToCompile("""import besom.model._
val n = QName("namespace/morenamespace/compl#x")
""")
failsToCompile("""import besom.model._
val n = Name("namespace/morenamespace/compl#x")
""")

test("parsing - simple name"):
assertEquals(Name("simple"), "simple")
assertEquals(QName("namespace/complex").name, "complex")
assertEquals(QName("ns1/ns2/ns3/ns4/complex").name, "complex")
assertEquals(QName("_/_/_/_/a0/c0Mpl3x_").name, "c0Mpl3x_")

test("parsing - simple namespace"):
assertEquals(QName("namespace/complex").namespace, "namespace")
assertEquals(QName("ns1/ns2/ns3/ns4/complex").namespace, "ns1/ns2/ns3/ns4")
assertEquals(QName("_/_/_/_/a0/c0Mpl3x_").namespace, "_/_/_/_/a0")

test("convert to QName"):
assertEquals(QName.convert("foo/bar"), "foo/bar")
assertEquals(QName.convert("https:"), "https_")
assertEquals(QName.convert("https://"), "https_")
assertEquals(QName.convert(""), "_")
assertEquals(QName.convert("///"), "_")

end NamesTest
13 changes: 13 additions & 0 deletions auto/src/test/scala/besom/test/CompileAssertions.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package besom.test

trait CompileAssertions:
self: munit.FunSuite =>

inline def failsToCompile(inline code: String): Unit =
assert(
!scala.compiletime.testing.typeChecks(code),
s"Code compiled correctly when expecting type errors:${System.lineSeparator()}$code"
)

inline def compiles(inline code: String): Unit =
assert(scala.compiletime.testing.typeChecks(code), s"Code failed to compile:${System.lineSeparator()}$code")
2 changes: 1 addition & 1 deletion core/project.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//> using scala "3.3.1"
//> using options "-java-output-version:11" "-Ysafe-init" "-Xmax-inlines:64"
//> using options "-feature" "-Werror" "-Wunused:all"
//> using options "-feature" "-Wunused:all"

//> using dep "org.virtuslab::besom-json:0.1.1-SNAPSHOT"

Expand Down
64 changes: 61 additions & 3 deletions core/src/main/scala/besom/types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ object types:
* @param s
* a resource type string to parse
* @return
* a [[ResourceType]] if the string is valid, otherwise an compile time error occurs
* a [[ResourceType]] if the string is valid, otherwise a compile time error occurs
*/

// validate that resource type contains two colons between three identifiers, special characters are allowed, for instance:
Expand Down Expand Up @@ -105,7 +105,7 @@ object types:
* @param s
* a provider type string to parse
* @return
* a [[ProviderType]] if the string is valid, otherwise an compile time error occurs
* a [[ProviderType]] if the string is valid, otherwise a compile time error occurs
*/
// validate that provider type contains a prefix of `pulumi:providers:` and the provider identifier
inline def from(s: String): ProviderType =
Expand All @@ -130,7 +130,7 @@ object types:
* @param s
* a function token string to parse
* @return
* a [[FunctionToken]] if the string is valid, otherwise an compile time error occurs
* a [[FunctionToken]] if the string is valid, otherwise a compile time error occurs
*/
// validate that function token contains two colons between three identifiers, see @ResourceType
inline def from(s: String): FunctionToken =
Expand Down Expand Up @@ -346,4 +346,62 @@ object types:
}

export besom.aliases.{*, given}

/** A stack name formatted with the greatest possible specificity: `org/project/stack` or `user/project/stack`
*
* Using this format avoids ambiguity in stack identity guards creating or selecting the wrong stack. Note that filestate backends (local
* file, S3, Azure Blob, etc.) do not support stack names, and instead prefixes the stack name with the word `organisation` and the project
* name, e.g. `organization/my-project-name/my-stack-name`.
*
* See: https://github.com/pulumi/pulumi/issues/2522
*/
opaque type FullyQualifiedStackName <: String = String

object FullyQualifiedStackName:

import scala.compiletime.*
import scala.compiletime.ops.string.*
import scala.language.implicitConversions

/** Parse a string into a [[FullyQualifiedStackName]].
*
* @param s
* is a string to parse
* @return
* a [[FullyQualifiedStackName]] if the string is valid, otherwise a compile time error occurs
*/
inline def apply(s: String): FullyQualifiedStackName =
requireConst(s)
inline if !constValue[Matches[s.type, ".+/.+/.+"]] then error("Invalid FullyQualifiedStackName string. Must match '.+/.+/.+'")
else s

implicit inline def str2FullyQualifiedStackName(inline s: String): FullyQualifiedStackName = FullyQualifiedStackName(s)

private[besom] def unsafeOf(s: String): FullyQualifiedStackName = s

extension (fqsn: FullyQualifiedStackName)
/** @return
* the [[FullyQualifiedStackName]] as a [[Tuple]] of [[String]]s, in the format `(org, project, stack)`
*/
def parts: (String, String, String) = fqsn match
case s"${org}/${project}/${stack}" => (org, project, stack)
case _ => throw IllegalArgumentException(s"Invalid FullyQualifiedStackName string: ${fqsn}")

/** @return
* the organisation name
*/
def organisation: String = parts._1

/** @return
* the project name
*/
def project: String = parts._2

/** @return
* the stack name
*/
def stack: String = parts._3

end FullyQualifiedStackName

end types

0 comments on commit b0395f0

Please sign in to comment.