Skip to content

Commit

Permalink
Fix clashing class names when generating Pkl from OpenAPI/JSON Schema (
Browse files Browse the repository at this point in the history
  • Loading branch information
bioball authored Feb 17, 2025
1 parent 5b4e443 commit fa1f4ad
Show file tree
Hide file tree
Showing 13 changed files with 454 additions and 76 deletions.
4 changes: 2 additions & 2 deletions packages/k8s.contrib.crd/PklProject
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//===----------------------------------------------------------------------===//
// Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
// Copyright © 2024-2025 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.
Expand Down Expand Up @@ -29,5 +29,5 @@ dependencies {
}

package {
version = "1.0.14"
version = "2.0.0"
}
2 changes: 1 addition & 1 deletion packages/k8s.contrib.crd/PklProject.deps.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
},
"package://pkg.pkl-lang.org/pkl-pantry/org.json_schema.contrib@1": {
"type": "local",
"uri": "projectpackage://pkg.pkl-lang.org/pkl-pantry/org.json_schema.contrib@1.1.1",
"uri": "projectpackage://pkg.pkl-lang.org/pkl-pantry/org.json_schema.contrib@1.1.2",
"path": "../org.json_schema.contrib"
},
"package://pkg.pkl-lang.org/pkl-pantry/pkl.experimental.syntax@1": {
Expand Down
59 changes: 44 additions & 15 deletions packages/k8s.contrib.crd/internal/ModuleGenerator.pkl
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//===----------------------------------------------------------------------===//
// Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
// Copyright © 2024-2025 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.
Expand All @@ -22,6 +22,7 @@ import "pkl:reflect"
import "@jsonschema.contrib/internal/Type.pkl"
import "@jsonschema.contrib/internal/TypesGenerator.pkl"
import "@jsonschema.contrib/internal/utils.pkl"
import "@jsonschema.contrib/internal/singularize.pkl"
import "@jsonschema/JsonSchema.pkl"
import "@jsonschema/Parser.pkl"
import "@k8s/apiextensions-apiserver/pkg/apis/apiextensions/v1/CustomResourceDefinition.pkl"
Expand Down Expand Up @@ -54,7 +55,7 @@ local schema: CustomResourceDefinition.CustomResourceValidation|BetaCRD.CustomRe
(if (crd is BetaCRD) version.schema ?? crd.spec.validation else version.schema)!!

/// The Schema
rootSchema: JsonSchema((s) -> validCRDSchema(s)) = Parser.parse(new JsonRenderer {}.renderDocument(schema.openAPIV3Schema)) as JsonSchema
rootSchema: JsonSchema(validCRDSchema(this)) = Parser.parse(new JsonRenderer {}.renderDocument(schema.openAPIV3Schema)) as JsonSchema

local ignoreProperties = Set("apiVersion", "kind", "metadata")
local filteredRootSchema = (rootSchema) {
Expand Down Expand Up @@ -280,31 +281,59 @@ function isClassLike(schema: JsonSchema.Schema): Boolean =
///
/// Try to use the parent property's name as part of the class name in case of conflict.
/// If already at the root, add a number at the end.
local function determineTypeName(path: List<String>, candidateName: String, existingTypeNames: Set<Type>, index: Int): Type =
if (existingTypeNames.contains(utils.pascalCase(candidateName)))
if (path.isEmpty)
determineTypeName(path, candidateName + index.toString(), existingTypeNames, index + 1)
local function determineTypeName(
path: List<String>,
candidateName: String,
existingTypeNames: Set<Type>,
index: Int
): Type =
let (candidate = utils.pascalCase(candidateName))
if (existingTypeNames.findOrNull((it) -> it.name == candidate) != null)
if (path.isEmpty)
determineTypeName(
path,
candidateName + index.toString(),
existingTypeNames,
index + 1
)
else
let (newPath = dropLast(path))
determineTypeName(
newPath,
getCandidateName(newPath) + candidate,
existingTypeNames,
index
)
else
determineTypeName(
path.dropLast(1),
utils.pascalCase(path.last.capitalize()) + utils.pascalCase(candidateName),
existingTypeNames,
index
)
new { name = candidate; moduleName = module.moduleName }

// noinspection TypeMismatch
local function getCandidateName(path: List<String>) =
if (path.isEmpty)
"Item"
else if (path.last == "[]")
path.dropLast(1).lastOrNull?.ifNonNull((it) -> utils.pascalCase(singularize.singularize(it))) ?? "Item"
else
utils.pascalCase(path.last)

local function dropLast(path: List<String>) =
if (path.last == "[]")
path.dropLast(2)
else
new { name = utils.pascalCase(candidateName); moduleName = module.moduleName }
path.dropLast(1)

/// The schemas that should be rendered as classes.
///
/// Classes get rendered for any subschema that has [JsonSchema.properties] defined, and does not show up in converters
local classSchemas: Type.TypeNames =
utils._findMatchingSubSchemas(filteredRootSchema, List(), (elem) -> elem != filteredRootSchema && isClassLike(elem))
.filter((path, _) -> !pathPrefixes(path).any((prefix) -> converters.containsKey(prefix))) // path or prefix are not explicitly in converters
// path or prefix are not explicitly in converters
.filter((path, _) -> !pathPrefixes(path).any((prefix) -> converters.containsKey(prefix)))
.entries
.fold(Map(), (accumulator: Type.TypeNames, pair) ->
let (path = pair.first)
let (schema = pair.second)
let (typeName = determineTypeName(path, path.lastOrNull?.capitalize() ?? "Item", accumulator.values.toSet(), 0))
let (typeName = determineTypeName(path, getCandidateName(path), accumulator.values.toSet(), 0))
accumulator.put(schema, typeName)
)

Expand Down
27 changes: 18 additions & 9 deletions packages/k8s.contrib.crd/tests/ModuleGenerator.pkl
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//===----------------------------------------------------------------------===//
// Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
// Copyright © 2024-2025 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.
Expand All @@ -17,9 +17,9 @@ module k8s.contrib.crd.tests.ModuleGenerator

amends "pkl:test"

import "package://pkg.pkl-lang.org/pkl-k8s/k8s@1.0.1#/api/core/v1/ResourceRequirements.pkl"
import "package://pkg.pkl-lang.org/pkl-k8s/k8s@1.0.1#/api/core/v1/EnvVar.pkl"
import "package://pkg.pkl-lang.org/pkl-k8s/k8s@1.0.1#/api/networking/v1/NetworkPolicy.pkl"
import "@k8s/api/core/v1/ResourceRequirements.pkl"
import "@k8s/api/core/v1/EnvVar.pkl"
import "@k8s/api/networking/v1/NetworkPolicy.pkl"

import "../generate.pkl"

Expand All @@ -28,11 +28,11 @@ local generator = (generate) {
source = "dummy://test_uri"
converters {
["restateclusters.restate.dev"] {
[List("spec", "compute", "env", "env")] = EnvVar
[List("spec", "compute", "env", "[]")] = EnvVar
[List("spec", "compute", "resources")] = ResourceRequirements
[List("spec", "security", "networkPeers", "ingress", "ingres")] = NetworkPolicy.NetworkPolicyPeer
[List("spec", "security", "networkPeers", "admin", "admin")] = NetworkPolicy.NetworkPolicyPeer
[List("spec", "security", "networkPeers", "metrics", "metric")] = NetworkPolicy.NetworkPolicyPeer
[List("spec", "security", "networkPeers", "ingress", "[]")] = NetworkPolicy.NetworkPolicyPeer
[List("spec", "security", "networkPeers", "admin", "[]")] = NetworkPolicy.NetworkPolicyPeer
[List("spec", "security", "networkPeers", "metrics", "[]")] = NetworkPolicy.NetworkPolicyPeer
}
}
}
Expand All @@ -59,7 +59,16 @@ examples {
}

for (filename, value in generator3.output.files!!) {
["\(filename).pkl -- different version of k8s"] {
["\(filename) -- different version of k8s"] {
value.text
}
}

["conflicting schemas"] {
for (_, value in (generate) {
sourceContents = read("fixtures/crds_conflict.yaml")
source = "dummy://test_uri"
}.output.files!!) {
value.text
}
}
Expand Down
80 changes: 79 additions & 1 deletion packages/k8s.contrib.crd/tests/ModuleGenerator.pkl-expected.pcf
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ examples {

"""
}
["RestateCluster.pkl.pkl -- different version of k8s"] {
["RestateCluster.pkl -- different version of k8s"] {
"""
/// Auto-generated derived type for RestateClusterSpec via `CustomResource`
///
Expand Down Expand Up @@ -294,6 +294,84 @@ examples {
storageRequestBytes: Int(this >= 1.0)
}

"""
}
["conflicting schemas"] {
"""
/// This module was generated from the CustomResourceDefinition at <dummy://test_uri>.
module bar.foo.v1.FooBar

extends "package://pkg.pkl-lang.org/pkl-k8s/k8s@1.0.1#/K8sResource.pkl"

import "package://pkg.pkl-lang.org/pkl-k8s/k8s@1.0.1#/apimachinery/pkg/apis/meta/v1/ObjectMeta.pkl"

fixed apiVersion: "foo.bar/v1"

fixed kind: "FooBar"

/// Standard object's metadata.
///
/// More info: <https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata>.
metadata: ObjectMeta?

foo: Foo?

class Foo {
/// Test nested objects field collision.
redObject: RedObject?

/// Test nested objects field collision.
blueObject: BlueObject?
}

/// Test nested objects field collision.
class RedObject {
/// Nested field.
nestedField: String

/// Nested child object.
nestedObject: NestedObject?

/// Nested list object red.
nestedList: Listing<NestedList>?
}

/// Nested child object.
class NestedObject {
/// Nested field.
nestedRed: String?
}

/// Red nested object test items.
class NestedList {
/// Red nested list field.
nestedListItemField: String?
}

/// Test nested objects field collision.
class BlueObject {
/// Nested field.
nestedField: String

/// Nested child object.
nestedObject: BlueObjectNestedObject?

/// Nested list object blue.
nestedList: Listing<BlueObjectNestedList>?
}

/// Nested child object.
class BlueObjectNestedObject {
/// Nested field.
nestedBlue: String?
}

/// Blue nested object test items.
class BlueObjectNestedList {
/// Blue nested list field.
nestedListItemField: String?
}

"""
}
}
81 changes: 81 additions & 0 deletions packages/k8s.contrib.crd/tests/fixtures/crds_conflict.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: foo.bar.com
spec:
group: foo.bar
names:
categories: []
kind: FooBar
plural: foobar
shortNames:
- fb
singular: foobar
scope: Cluster
versions:
- additionalPrinterColumns: []
name: v1
schema:
openAPIV3Schema:
properties:
foo:
properties:
redObject:
description: Test nested objects field collision.
properties:
nestedField:
description: Nested field.
type: string
nestedObject:
description: Nested child object.
properties:
nestedRed:
description: Nested field.
type: string
type: object
nestedList:
description: Nested list object red.
items:
description: Red nested object test items.
properties:
nestedListItemField:
description: Red nested list field.
type: string
type: object
nullable: true
type: array
required:
- nestedField
type: object
blueObject:
description: Test nested objects field collision.
properties:
nestedField:
description: Nested field.
type: string
nestedObject:
description: Nested child object.
properties:
nestedBlue:
description: Nested field.
type: string
type: object
nestedList:
description: Nested list object blue.
items:
description: Blue nested object test items.
properties:
nestedListItemField:
description: Blue nested list field.
type: string
type: object
nullable: true
type: array
required:
- nestedField
type: object
required:
- image
type: object
served: true
storage: true
4 changes: 2 additions & 2 deletions packages/org.json_schema.contrib/PklProject
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//===----------------------------------------------------------------------===//
// Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
// Copyright © 2024-2025 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.
Expand All @@ -25,5 +25,5 @@ dependencies {
}

package {
version = "1.1.1"
version = "1.1.2"
}
Loading

0 comments on commit fa1f4ad

Please sign in to comment.