Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PowerShell and PSCustomObject #540

Open
bozho opened this issue Mar 21, 2024 · 6 comments
Open

PowerShell and PSCustomObject #540

bozho opened this issue Mar 21, 2024 · 6 comments
Labels

Comments

@bozho
Copy link

bozho commented Mar 21, 2024

Hi!

I'm trying to use scriban in Windows PowerShell 5.1 and am having issues with rendering PSCustomObject objects.

Using hastable and OrderedDictionary works fine, e.g.:

$a = [Ordered]@{ foo = "bar" }
$template = [Scriban.Template]::Parse('hello {{foo}}')
$template.Render($a)

hello bar

My problem is that ConvertFrom-Json cmdlet returns a PSCustomObject and not a hashtable (that option is available in PS 6/7, but not in 5.1), and I'd like to use parsed JSON files as input to some of my templates.

The trivial approach above does not work, which is expected, since PSCustomObject is not a dictionary/hashtable.

Next, I tried the "import .NET object instance" approach:

$a = [PSCustomObject]@{ foo = "bar" }
$scriptObject = [Scriban.Runtime.ScriptObject]::new()
[Scriban.Runtime.ScriptObjectExtensions]::Import($scriptObject, $a) # this is how you call extension methods in PS
$context = [Scriban.TemplateContext]::new()
$context.PushGlobal($scriptObject)
$template = [Scriban.Template]::Parse('hello {{foo}}')
$template.Render($context)

hello

$scriptObject.properties lists foo as a member:

$scriptObject.properties

MemberType      : NoteProperty
IsSettable      : True
IsGettable      : True
Value           : bar
TypeNameOfValue : System.String
Name            : foo
IsInstance      : True

$context.CurrentGlobal also looks ok:

$context.CurrentGlobal

Key                   Value
---                   -----
base_object
members               {string foo=bar, string ToString(), bool Equals(System.Object obj), int GetHashCode()...}
properties            {string foo=bar}
methods               {string ToString(), bool Equals(System.Object obj), int GetHashCode(), type GetType()}
immediate_base_object
type_names            {System.Management.Automation.PSCustomObject, System.Object}

Am I doing something wrong here, or does scriban not handle NoteProperty members?

Thank you!

@xoofx
Copy link
Member

xoofx commented Mar 31, 2024

Shouldn't it be more something like the following?

[Scriban.Runtime.ScriptObjectExtensions]::Import($scriptObject.properties, $a) # this is how you call extension methods in PS

Not sure to follow why you would want to import all the metadatas of PSCustomObject, as [Scriban.Runtime.ScriptObjectExtensions]::Import is not aware about the particular structure of this PS objects, you need to give it some help, no?

@xoofx xoofx added the question label Mar 31, 2024
@bozho
Copy link
Author

bozho commented Apr 2, 2024

@xoofx I was going off this example in the docs:

var scriptObject1 = new ScriptObject();
scriptObject1.Import(new MyObject());

var context = new TemplateContext();
context.PushGlobal(scriptObject1);

var template = Template.Parse("This is Hello: `{{hello}}`");
var result = template.Render(context);

// Prints This is MyFunctions.Hello: `hello from method!`
Console.WriteLine(result);

Where the ScriptObject static extension method Import is used to import properties for a .NET object (MyObject).

In PS,

[Scriban.Runtime.ScriptObjectExtensions]::Import($scriptObject, $a)

is the equivalent of doing

scriptObject.Import(a)

in C#.

@xoofx
Copy link
Member

xoofx commented Apr 2, 2024

Sorry, mixed the target object and the parameter:

[Scriban.Runtime.ScriptObjectExtensions]::Import($scriptObject, $a.properties)

The problem is that it looks like you are importing the wrong thing. The $a object is of type PSCustomObject, which seems to have e.g a properties which actually contain the properties. I would believe that's what you should import. Not the whole PSCustomObject that PowerShell specific.

@bozho
Copy link
Author

bozho commented Apr 2, 2024

To access PSCustomObject properties, you have to use $a.PSObject.Properties, which is of type PSMemberInfoIntegratingCollection, which seems to throws ScriptObject completely off, since both $scriptObject.properties and $context.CurrentGlobal are null.

The confusing thing is that when I use [Scriban.Runtime.ScriptObjectExtensions]::Import($scriptObject, $a), both $scriptObject.properties and $context.CurrentGlobal look as expected: the foo property is there, as well as its value, but the template is not rendered as expected.

Maybe the renderer fails to fetch the value correctly because foo is a NoteProperty? Can I write a template to print out all known variable names and values?

NB: This is not a huge issue, since I have a function to perform a simple conversion from a PSCustomObject to a hashtable and ConvertFrom-Json function in PS 6+ has the -AsHashtable parameter for parsing json.

@xoofx
Copy link
Member

xoofx commented Apr 2, 2024

The confusing thing is that when I use [Scriban.Runtime.ScriptObjectExtensions]::Import($scriptObject, $a), both $scriptObject.properties and $context.CurrentGlobal look as expected: the foo property is there, as well as its value, but the template is not rendered as expected.

They are not. Only properties should be imported.

Because otherwise you have to modify your script to go through properties:

$template = [Scriban.Template]::Parse('hello {{properties.foo}}')

That's why I'm suggesting to pass $a.properties because $context.CurrentGlobal looks wrong.

It should be instead something like:

$context.CurrentGlobal

Key                   Value
---                   -----
foo                   bar

@bozho
Copy link
Author

bozho commented Apr 2, 2024

Ok, I think I've got it.

I've tested it with a regular C# class and it worked as expected.

The problem is that PSCustomObject properties are not of MemberType Property, but NoteProperty, so using Import imports actual PSObject properties (base_object, properties, etc.)

Without Scriban treating NoteProperty members as regular properties, the correct approach is:

$a = [PSCustomObject]@{ foo = "bar" }
$scriptObject = [Scriban.Runtime.ScriptObject]::new()
$a.PSObject.Properties | ForEach-Object {
    $scriptObject.Add($_.Name, $_.Value)
}
$context = [Scriban.TemplateContext]::new()
$context.PushGlobal($scriptObject)
$template = [Scriban.Template]::Parse('hello {{foo}}')
$template.Render($context)

This is a naive implementation, since it doesn't handle nested PSCustomObject properties.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants