Skip to content

Commit b4ec164

Browse files
committed
Merge remote-tracking branch 'upstream/main' into angular-explorer-08-support
2 parents 70e3436 + 40c6b94 commit b4ec164

2 files changed

Lines changed: 90 additions & 37 deletions

File tree

agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/core/schema/Catalog.kt

Lines changed: 90 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -301,45 +301,114 @@ data class A2uiCatalog(
301301
append("\n${A2uiConstants.A2UI_SCHEMA_BLOCK_END}")
302302
}
303303

304-
/** Loads and validates examples from a directory. */
304+
/** Loads and validates examples from a directory or a glob pattern. */
305305
@JvmOverloads
306306
fun loadExamples(path: String?, validate: Boolean = false): String {
307307
if (path.isNullOrEmpty()) return ""
308-
val dir = File(path)
309-
if (!dir.isDirectory) {
310-
logger.warning("Example path $path is not a directory")
308+
309+
val isDir = File(path).isDirectory
310+
val pattern =
311+
if (isDir) {
312+
val sep = if (path.endsWith("/") || path.endsWith(File.separator)) "" else "/"
313+
"$path$sep*.json"
314+
} else {
315+
path
316+
}
317+
318+
// Extract the base directory to avoid walking the entire filesystem.
319+
val firstWildcard = pattern.indexOfFirst { it == '*' || it == '?' || it == '[' }
320+
val baseDirPath =
321+
if (firstWildcard != -1) {
322+
val lastSlash = pattern.lastIndexOfAny(charArrayOf('/', '\\'), firstWildcard)
323+
if (lastSlash != -1) {
324+
pattern.substring(startIndex = 0, endIndex = lastSlash)
325+
} else {
326+
""
327+
}
328+
} else {
329+
if (isDir) {
330+
path
331+
} else {
332+
val parent = File(path).parent
333+
parent ?: ""
334+
}
335+
}
336+
337+
val baseDirFile = if (baseDirPath.isEmpty()) File(".") else File(baseDirPath)
338+
val matchedFiles = mutableListOf<File>()
339+
340+
if (baseDirFile.exists() && baseDirFile.isDirectory) {
341+
try {
342+
val matcher = java.nio.file.FileSystems.getDefault().getPathMatcher("glob:$pattern")
343+
// To support globstar matching where ** matches zero directories, create an alternate
344+
// matcher.
345+
val altPattern =
346+
pattern
347+
.replace(oldValue = "/**/", newValue = "/")
348+
.replace(regex = "^\\*\\*/".toRegex(), replacement = "")
349+
val altMatcher =
350+
if (altPattern != pattern) {
351+
java.nio.file.FileSystems.getDefault().getPathMatcher("glob:$altPattern")
352+
} else {
353+
null
354+
}
355+
356+
val startPath =
357+
if (baseDirPath.isEmpty()) {
358+
java.nio.file.Paths.get("")
359+
} else {
360+
java.nio.file.Paths.get(baseDirPath)
361+
}
362+
363+
java.nio.file.Files.walk(startPath).use { stream ->
364+
stream.forEach { p ->
365+
if (java.nio.file.Files.isRegularFile(p)) {
366+
if (matcher.matches(p) || altMatcher?.matches(p) == true) {
367+
matchedFiles.add(p.toFile())
368+
}
369+
}
370+
}
371+
}
372+
} catch (e: Exception) {
373+
logger.warning("Error walking files for pattern $pattern: ${e.message}")
374+
}
375+
}
376+
377+
if (matchedFiles.isEmpty()) {
378+
if (!isDir && !path.any { it == '*' || it == '?' || it == '[' }) {
379+
logger.warning("Example path $path is neither a directory nor a valid glob pattern")
380+
}
311381
return ""
312382
}
313383

314-
// Sort files by name to ensure deterministic output order for tests.
315-
val files =
316-
dir.listFiles { _, name -> name.endsWith(".json") }?.sortedBy { it.name } ?: emptyList<File>()
384+
// Sort files alphabetically by path to ensure deterministic output order and logical grouping.
385+
val files = matchedFiles.sortedBy { it.path }
317386

318387
return files
319388
.mapNotNull { file ->
320389
val basename = file.nameWithoutExtension
321-
try {
322-
val content = file.readText()
323-
if (validate && !validateExample(file.path, content)) {
324-
null
325-
} else {
326-
"---BEGIN $basename---\n$content\n---END $basename---"
390+
val content =
391+
try {
392+
file.readText()
393+
} catch (e: Exception) {
394+
logger.warning("Failed to read example ${file.path}: ${e.message}")
395+
return@mapNotNull null
327396
}
328-
} catch (e: Exception) {
329-
logger.warning("Failed to load example ${file.path}: ${e.message}")
330-
null
397+
398+
if (validate) {
399+
validateExample(file.path, content)
331400
}
401+
"---BEGIN $basename---\n$content\n---END $basename---"
332402
}
333-
.joinToString("\n\n")
403+
.joinToString(separator = "\n\n")
334404
}
335405

336-
private fun validateExample(fullPath: String, content: String): Boolean =
406+
private fun validateExample(fullPath: String, content: String) {
337407
try {
338408
val jsonElement = Json.parseToJsonElement(content)
339409
validator.validate(jsonElement)
340-
true
341410
} catch (e: Exception) {
342-
logger.warning("Failed to validate example $fullPath: ${e.message}")
343-
false
411+
throw IllegalArgumentException("Failed to validate example $fullPath: ${e.message}", e)
344412
}
413+
}
345414
}

agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/conformance/ConformanceTest.kt

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -242,22 +242,6 @@ class ConformanceTest {
242242
val action = case[ConformanceTestHelper.KEY_ACTION] as String
243243
val args = case[ConformanceTestHelper.KEY_ARGS] as? Map<*, *> ?: emptyMap<Any, Any>()
244244

245-
// Filter out non-conformant tests for Kotlin
246-
if (
247-
action == "load" &&
248-
(args[KEY_PATH] as? String)?.let {
249-
it.contains("*") || it.contains("[") || it.contains("?")
250-
} == true
251-
) {
252-
println("Skipping non-conformant test (load with glob): $name")
253-
return@mapNotNull null
254-
}
255-
if (action == "load" && case.containsKey(ConformanceTestHelper.KEY_EXPECT_ERROR)) {
256-
// Kotlin loadExamples skips invalid files instead of throwing, so it's not conformant with
257-
// error expectation
258-
println("Skipping non-conformant test (load expecting error): $name")
259-
return@mapNotNull null
260-
}
261245
DynamicTest.dynamicTest(name) {
262246
val catalog =
263247
(case[ConformanceTestHelper.KEY_CATALOG] as? Map<*, *>)?.let {

0 commit comments

Comments
 (0)