Skip to content
Draft
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
26 changes: 25 additions & 1 deletion mock/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,36 @@ import (
"github.com/extism/go-pdk"
)

//go:export reflectMapHost
func reflectMapHost(kPtr uint64) uint64 {
kMem := pdk.FindMemory(kPtr)
k := string(kMem.ReadBytes())

var obj map[string]interface{}
err := pdk.JSONFrom(kPtr, &obj)
if err != nil {
pdk.SetError(err)
return 0
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we link the test imports (e.g. assert, etc) into XTP Tests, I wonder if it would be worthwhile to run assertions on the kv pairs, doing a type assertion inside the mock here too?

e.g.

import (
    xtptest "github.com/dylibso/xtp-test-go"
)
...

a, ok := obj["keyFromJs"].(int)
xtptest.AssertEq("reflectMapHost preserved map int value", a, 42)

...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should work, assuming all the xtp CLI PRs are merged and the latest version is relased

fmt.Println(k)

kRet := pdk.AllocateString(k)
return kRet.Offset()
}

//go:export reflectJsonObjectHost
func reflectJsonObjectHost(kPtr uint64) uint64 {
kMem := pdk.FindMemory(kPtr)
k := string(kMem.ReadBytes())

// TODO should validate that we get json by trying to parse it
var obj map[string]interface{}
err := pdk.JSONFrom(kPtr, &obj)
if err != nil {
pdk.SetError(err)
return 0
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above re: adding the tests for type conversion / assertion now that the test module imports are linked.


fmt.Println(k)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From an older commit, but does this Println need to be here?


kRet := pdk.AllocateString(k)
Expand Down
74 changes: 64 additions & 10 deletions runner/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ const EmbeddedObject = {
anEnumArray: ["option1", "option2", "option3"],
anIntArray: [1, 2, 3],
aDate: "2024-07-23T16:03:34.000Z",
anIntMap: {
'int1': 101,
'int2': 108,
},
aDateMap: {
'date1': "2024-07-23T16:03:34.000Z",
'date2': "2024-07-23T16:03:34.000Z",
},
anEnumMap: {
'enum1': "option1",
'enum2': "option2",
},
};

const inputBufferString = "Hello 🌍 World!🌍";
Expand All @@ -30,29 +42,63 @@ const KitchenSink = {
new TextEncoder().encode(inputBufferString).buffer,
),
// aBuffer: "NzIsMTAxLDEwOCwxMDgsMTExLDMyLDI0MCwxNTksMTQwLDE0MSwzMiw4NywxMTEsMTE0LDEwOCwxMDAsMzMsMjQwLDE1OSwxNDAsMTQx",
aStringMap: {
'str1': "Hello",
'str2': "🌍",
'str3': "World!",
},
anObjectMap: {
'obj1': EmbeddedObject,
'obj2': EmbeddedObject,
},
anArrayMap: {
'array1': [EmbeddedObject],
'array2': [EmbeddedObject],
},
};

export function test() {
Test.group("schema v1-draft encoding types", () => {
let input = JSON.stringify(KitchenSink);

let output: typeof KitchenSink = Test.call("reflectJsonObject", input)
.json();

matchIdenticalTopLevel(output);
matchIdenticalEmbedded(output.anEmbeddedObject);
output.anEmbeddedObjectArray.forEach(matchIdenticalEmbedded);
matchIdenticalTopLevel("reflectJsonObject", output);
matchIdenticalEmbedded("reflectJsonObject", output.anEmbeddedObject);
output.anEmbeddedObjectArray.forEach(x => matchIdenticalEmbedded("reflectJsonObject:anEmbeddedObjectArray", x));
Object.entries(output.anObjectMap).forEach(([i, v]) => matchIdenticalEmbedded(`reflectJsonObject:anObjectMap:${i}`, v));
Object.entries(output.anArrayMap).forEach(([i, v]) => matchIdenticalEmbedded(`reflectJsonObject:anArrayMap:${i}`, v[0]));

// dates and JSON encodings between languages are a little fuzzy.
// so, rather than test stringified equality, we test the value of
// the date in various forms.
matchDate(output);
matchDate("reflectJsonObject", output);

Test.assertEqual(
"reflectJsonObject preserved optional field semantics",
output.anOptionalString || null,
KitchenSink.anOptionalString,
);

let inputM = JSON.stringify({ "k1": [KitchenSink], "k2": [KitchenSink] });
let outputM: Record<string, typeof KitchenSink[]> = Test.call("reflectMap", inputM)
.json()

for (const [k, v] of Object.entries(outputM)) {
const m = v[0];
matchIdenticalTopLevel(`reflectMap:${k}`, m);
matchIdenticalEmbedded(`reflectMap:${k}`, m.anEmbeddedObject);
m.anEmbeddedObjectArray.forEach((x, i) => matchIdenticalEmbedded(`reflectMap:${k}:anEmbeddedObjectArray${i}`, x));
matchDate(`reflectMap:${k}`, m);
}

Test.assertEqual(
`reflectMap preserved optional field semantics`,
outputM["k1"][0].anOptionalString || null,
KitchenSink.anOptionalString
)

let inputS = KitchenSink.aString;
let outputS = Test.call("reflectUtf8String", inputS).text();
Test.assertEqual("reflectUtf8String preserved the string", outputS, inputS);
Expand Down Expand Up @@ -126,7 +172,7 @@ export function test() {
return 0;
}

const matchIdenticalTopLevel = (output: any) => {
const matchIdenticalTopLevel = (func: string, output: any) => {
// determine top-level fields that should be identical
// NOTE: anEmbeddedObject, anEmbeddedObjectArray, and aDate are intentionally omitted here
const matchIdentical = [
Expand All @@ -138,6 +184,7 @@ const matchIdenticalTopLevel = (output: any) => {
"anUntypedObject",
"anEnum",
"aBuffer",
"aStringMap",
] as const;
matchIdentical.forEach((k: typeof matchIdentical[number]) => {
let key: keyof typeof KitchenSink = k;
Expand All @@ -153,40 +200,47 @@ const matchIdenticalTopLevel = (output: any) => {
if (actual === undefined) {
actual = null;
}
} else if (key === "aStringMap") {
actual = JSON.stringify(actual);
expected = JSON.stringify(expected);
}

Test.assertEqual(
`reflectJsonObject preserved identical value '${key}'`,
`${func} preserved identical value '${key}'`,
actual,
expected,
);
});
};

const matchIdenticalEmbedded = (embedded: any) => {
const matchIdenticalEmbedded = (func: string, embedded: any) => {
// determine flat embedded items that should be identical
// NOTE: aDate is intentionally omitted here
const matchIdenticalEmbedded = [
"aBoolArray",
"aStringArray",
"anEnumArray",
"anIntArray",
"anIntMap",
"aDateMap",
"anEnumMap",
] as const;
matchIdenticalEmbedded.forEach((k: typeof matchIdenticalEmbedded[number]) => {
let key: keyof typeof EmbeddedObject = k;
Test.assertEqual(
`reflectJsonObject preserved identical value '${key}'`,
`${func} preserved identical value '${key}'`,
JSON.stringify(embedded[k]),
JSON.stringify(EmbeddedObject[key]),
);
});
};

const matchDate = (output: any) => {
const matchDate = (func: string, output: any) => {
let expected = new Date(KitchenSink.aDate);
let actual = new Date(output.aDate);

Test.assertEqual(
`reflectJsonObject preserves semantics of 'date-time' formatted value`,
`${func} preserves semantics of 'date-time' formatted value`,
actual.getTime(),
expected.getTime(),
);
Expand Down
67 changes: 67 additions & 0 deletions schema.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
version: v1-draft
exports:
reflectMap:
description: |
This function takes a map and returns the same map.
codeSamples:
- lang: typescript
source: |
// pass this through the host function and return it back
return reflectMapHost(input)
input:
contentType: application/json
additionalProperties:
type: array
items:
$ref: '#/components/schemas/KitchenSinkObject'
output:
contentType: application/json
additionalProperties:
type: array
items:
$ref: '#/components/schemas/KitchenSinkObject'
reflectJsonObject:
description: |
This function takes a KitchenSinkObject and returns a KitchenSinkObject.
Expand Down Expand Up @@ -276,6 +296,24 @@ exports:
input.aBuffer = input.aBuffer.replace(b"Hello", b"Goodbye"); return input

imports:
reflectMapHost:
description: |
This function takes a Map and returns a Map.
It should come out hte same way it came in. It's the same as the export.
But the export should call this.
input:
contentType: application/json
additionalProperties:
type: array
items:
$ref: '#/components/schemas/KitchenSinkObject'
output:
contentType: application/json
additionalProperties:
type: array
items:
$ref: '#/components/schemas/KitchenSinkObject'

reflectJsonObjectHost:
description: |
This function takes a KitchenSinkObject and returns a KitchenSinkObject.
Expand Down Expand Up @@ -356,6 +394,12 @@ components:
description: a date
type: string
format: date-time
aDateMap:
description: a map of integers
type: object
additionalProperties:
type: string
format: date-time
AStringEnum:
description: A string enum
type: string
Expand Down Expand Up @@ -408,3 +452,26 @@ components:
aBuffer:
description: a byte buffer
type: buffer
aStringMap:
description: a map of strings
type: object
additionalProperties:
type: string
anObjectMap:
description: a map of objects
type: object
additionalProperties:
$ref: "#/components/schemas/EmbeddedObject"
anArrayMap:
description: a map of arrays
type: object
additionalProperties:
type: array
items:
$ref: "#/components/schemas/EmbeddedObject"
anEnumMap:
description: a map of enums
type: object
additionalProperties:
$ref: "#/components/schemas/AStringEnum"