Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ ts/create-kpt-functions/bin
*.swo
*.swp

# rapid property-based testing failure reproductions
**/testdata/rapid/

7 changes: 5 additions & 2 deletions go/fn/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ require (
sigs.k8s.io/kustomize/kyaml v0.21.1
)

require github.com/pkg/errors v0.9.1
require (
github.com/pkg/errors v0.9.1
go.yaml.in/yaml/v3 v3.0.4
pgregory.net/rapid v1.3.0
)

require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
Expand All @@ -39,7 +43,6 @@ require (
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/sys v0.44.0 // indirect
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go/fn/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
k8s.io/kube-openapi v0.0.0-20260520065146-aa012df4f4af h1:zLXA2Irn14q2/06WMkxViyr7YCPUO2lJ0QYE9Juy5vA=
k8s.io/kube-openapi v0.0.0-20260520065146-aa012df4f4af/go.mod h1:V/QaCUYDa+0QpcHhVVc5l99Uz56wEMEXBSj9oCDkNDY=
pgregory.net/rapid v1.3.0 h1:vBvO0VSqti75J1jjYqpgPNBLKMd1+gxa9fYo7vk/Exc=
pgregory.net/rapid v1.3.0/go.mod h1:dPlE4OBBxgXPqkP79flB6sJL1dx5azpI7HQ9MY9Z7uk=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7fI=
Expand Down
82 changes: 82 additions & 0 deletions go/fn/internal/docs/markers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright 2025 The kpt Authors
//
// 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
//
// http://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.

package docs

import "strings"

// Sections holds parsed README marker content.
type Sections struct {
Short string
Long string
Examples string
}

const (
markerShort = "<!--mdtogo:Short-->"
markerLong = "<!--mdtogo:Long-->"
markerExamples = "<!--mdtogo:Examples-->"
markerEnd = "<!--mdtogo-->"
)

// ParseMarkers extracts mdtogo marker sections from README content.
// Missing markers result in empty strings for the corresponding sections.
// When no markers are present at all, the full content (trimmed) is returned
// as the Long description.
func ParseMarkers(readme []byte) Sections {
content := string(readme)

short := extractSection(content, markerShort)
long := extractSection(content, markerLong)
examples := extractSection(content, markerExamples)

// Fallback: if no markers are present at all, use full content as Long.
if !hasAnyMarker(content) {
return Sections{
Long: strings.TrimSpace(content),
}
}

return Sections{
Short: short,
Long: long,
Examples: examples,
}
}

// extractSection finds text between the given start marker and the next
// <!--mdtogo--> end marker. Returns empty string if either marker is missing.
func extractSection(content, startMarker string) string {
startIdx := strings.Index(content, startMarker)
if startIdx < 0 {
return ""
}
afterStart := startIdx + len(startMarker)
remaining := content[afterStart:]

before, _, ok := strings.Cut(remaining, markerEnd)
if !ok {
return ""
}

return strings.TrimSpace(before)
}

// hasAnyMarker reports whether the content contains any mdtogo marker.
func hasAnyMarker(content string) bool {
return strings.Contains(content, markerShort) ||
strings.Contains(content, markerLong) ||
strings.Contains(content, markerExamples) ||
strings.Contains(content, markerEnd)
}
119 changes: 119 additions & 0 deletions go/fn/internal/docs/markers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright 2025 The kpt Authors
//
// 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
//
// http://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.

package docs

import (
"fmt"
"strings"
"testing"

"pgregory.net/rapid"
)

// Feature: sdk-alignment, Property 1: Marker parser round-trip
//
// For any three strings (short, long, examples), formatting them into a README
// with mdtogo markers and then parsing that README with ParseMarkers SHALL
// produce a Sections struct with fields equal to the original strings (after trimming).
//
// Validates: Requirements 6.1, 6.2, 6.3, 6.5, 5.2

// genSectionContent generates arbitrary non-empty strings that do not contain
// mdtogo markers (which would confuse the parser).
func genSectionContent() *rapid.Generator[string] {
return rapid.Custom(func(t *rapid.T) string {
s := rapid.StringMatching(`[a-zA-Z0-9 \t\n.,;:!?(){}\[\]'"/_-]{1,200}`).Draw(t, "content")
// Ensure the generated content does not accidentally contain marker strings.
s = strings.ReplaceAll(s, "<!--mdtogo:", "")
s = strings.ReplaceAll(s, "<!--", "")
return s
})
}

// formatMarkedREADME formats three section strings into a README with mdtogo markers.
func formatMarkedREADME(short, long, examples string) string {
return fmt.Sprintf(`<!--mdtogo:Short-->
%s
<!--mdtogo-->

<!--mdtogo:Long-->
%s
<!--mdtogo-->

<!--mdtogo:Examples-->
%s
<!--mdtogo-->
`, short, long, examples)
}

// Feature: sdk-alignment, Property 7: Missing markers fallback
//
// For any README content that does NOT contain mdtogo markers, ParseMarkers
// SHALL return empty strings for Short and Examples, and the full content
// (trimmed) as Long.
//
// Validates: Requirements 5.4, 6.4

// genNoMarkerContent generates arbitrary strings guaranteed not to contain
// any mdtogo marker substrings.
func genNoMarkerContent() *rapid.Generator[string] {
return rapid.Custom(func(t *rapid.T) string {
s := rapid.StringMatching(`[a-zA-Z0-9 \t\n.,;:!?(){}\[\]'"/_-]{0,300}`).Draw(t, "content")
// Strip anything that could form a marker.
s = strings.ReplaceAll(s, "<!--mdtogo:", "")
s = strings.ReplaceAll(s, "<!--mdtogo", "")
s = strings.ReplaceAll(s, "<!--", "")
return s
})
}

func TestProperty7_MissingMarkersFallback(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
content := genNoMarkerContent().Draw(t, "readme")

sections := ParseMarkers([]byte(content))

if sections.Short != "" {
t.Fatalf("Short should be empty for content without markers, got: %q", sections.Short)
}
if sections.Examples != "" {
t.Fatalf("Examples should be empty for content without markers, got: %q", sections.Examples)
}
if got, want := sections.Long, strings.TrimSpace(content); got != want {
t.Fatalf("Long mismatch:\n got: %q\n want: %q", got, want)
}
})
}

func TestProperty1_MarkerParserRoundTrip(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
short := genSectionContent().Draw(t, "short")
long := genSectionContent().Draw(t, "long")
examples := genSectionContent().Draw(t, "examples")

readme := formatMarkedREADME(short, long, examples)
sections := ParseMarkers([]byte(readme))

if got, want := sections.Short, strings.TrimSpace(short); got != want {
t.Fatalf("Short mismatch:\n got: %q\n want: %q", got, want)
}
if got, want := sections.Long, strings.TrimSpace(long); got != want {
t.Fatalf("Long mismatch:\n got: %q\n want: %q", got, want)
}
if got, want := sections.Examples, strings.TrimSpace(examples); got != want {
t.Fatalf("Examples mismatch:\n got: %q\n want: %q", got, want)
}
})
}
39 changes: 39 additions & 0 deletions go/fn/internal/docs/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright 2025 The kpt Authors
//
// 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
//
// http://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.

package docs

import "go.yaml.in/yaml/v3"

// Metadata holds parsed metadata.yaml content.
type Metadata struct {
Image string `yaml:"image" json:"image"`
Description string `yaml:"description" json:"description"`
Tags []string `yaml:"tags" json:"tags"`
SourceURL string `yaml:"sourceURL" json:"sourceURL"`
ExamplePackageURLs []string `yaml:"examplePackageURLs" json:"examplePackageURLs"`
License string `yaml:"license" json:"license"`
Hidden bool `yaml:"hidden" json:"hidden"`
}

// ParseMetadata parses metadata.yaml content into a Metadata struct.
// Returns zero-value Metadata and an error if YAML is invalid.
// Returns successfully with partial fields if optional fields are missing.
func ParseMetadata(meta []byte) (Metadata, error) {
var m Metadata
if err := yaml.Unmarshal(meta, &m); err != nil {
return Metadata{}, err
}
return m, nil
}
Loading
Loading