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

How to forcefully interpret as an array with implied key #830

Closed
timheuer opened this issue Aug 3, 2023 · 3 comments
Closed

How to forcefully interpret as an array with implied key #830

timheuer opened this issue Aug 3, 2023 · 3 comments
Labels

Comments

@timheuer
Copy link

timheuer commented Aug 3, 2023

I'm trying to parse a GitHub Actions workflow file...here's a snippet of valid workflow:

name: "Build"

on:
  workflow_dispatch:
    inputs:
      reason:
        description: The reason for the manual run
        required: true
        type: string
      something_else:
        description: Some other input
        required: false
        type: boolean

The inputs is not resolving to an array, but rather properties of inputs. If I change these to -reason etc then it sees them as an array, but that's not valid workflow syntax.

I'm curious if there is a way I can get the parser to see this as an array where the reason portion would be the key?

@EdwardCooke
Copy link
Collaborator

The reasons it’s not coming in as an array is because it isn’t an array. It’s a mapping. Which means it will be properties or key/value pairs. You could probably create your own node deserializer or type converter to handle that part of your yaml differently.

You can see here what the different parts of your yaml are with this cool tool. It’s what we use to determine the validity of yaml and one of the tools we use to make sure we’re parsing things correctly.

http://ben-kiki.org/ypaste/data/78777/index.html

@tymokvo
Copy link
Contributor

tymokvo commented Aug 11, 2023

I think the short answer to your question is "no" since arrays don't have string keys (like reason) but you could make a type/named tuple that can hold the key and value from the mapping in one value.

It seems a lot easier to deserialize the way the library intends and then just implement a method on your type that returns the array-ified (key, value) pairs. I made a quick version with F#, but the C# version should be broadly similar. Except more verbose 😉.

Some gross F# code
#r "nuget: YamlDotNet"

open YamlDotNet.Serialization
open System.IO

type Input() =
    member val description = "" with get, set
    member val required = false with get, set

    [<YamlMember(Alias = "type")>]
    member val kind = "" with get, set

    member z.toRecord =
        {| description = z.description
           required = z.required
           kind = z.kind |}

type WorkflowDispatch() =
    member val inputs: System.Collections.Generic.Dictionary<string, Input> =
        System.Collections.Generic.Dictionary() with get, set

    member z.inputsArray =
        z.inputs.Keys
        |> Seq.map (fun k ->
            {| name = k
               value = z.inputs[k].toRecord |})
        |> Seq.toArray

type On() =
    member val workflowDispatch = WorkflowDispatch() with get, set

type GHAction() =
    member val name = "" with get, set
    member val on = On() with get, set

    member z.inputsArray = z.on.workflowDispatch.inputsArray

/// Create a deserializer for a YAML file
let deserializer _ =
    DeserializerBuilder()
        .WithNamingConvention(NamingConventions.UnderscoredNamingConvention.Instance)
        .Build()

let deserialize<'t> (content: string) =
    content |> (() |> deserializer).Deserialize<'t>

File.ReadAllText("ghaction.yml")
|> deserialize<GHAction>
|> (fun gha -> gha.inputsArray)
|> printfn "%A"

result:

[| { name = "reason"
     value =
       { description = "The reason for the manual run"
         kind = "string"
         required = true } }
   { name = "something_else"
     value =
       { description = "Some other input"
         kind = "boolean"
         required = false } } |]

@EdwardCooke
Copy link
Collaborator

Ok, spent some time on this, you can use a custom INodeDeserializer to convert it into an array. I set the value to the Key property on the object. Here's the code.

Note, it does not deserialize to the same yaml, if you need that, let me know and I could try and put something together for that.

using YamlDotNet.Core;
using YamlDotNet.Core.Events;
using YamlDotNet.Serialization;

var yaml = @"name: ""Build""
on:
  workflow_dispatch:
    inputs:
      reason:
        description: The reason for the manual run
        required: true
        type: string
      something_else:
        description: Some other input
        required: false
        type: boolean
";
var deserializer = new DeserializerBuilder().WithNodeDeserializer(new InputObjectNodeDeserializer(), (syntax) => syntax.OnTop()).Build();

var action = deserializer.Deserialize<Actions>(yaml);
foreach (var input in action.On.WorkflowDispatch.Inputs)
{
    Console.WriteLine("{0} - {1}", input.Key, input.Description);
}

public class Actions
{
    [YamlMember(Alias = "name")]
    public string Name { get; set; }

    [YamlMember(Alias = "on")]
    public Dispatch On { get; set; }

}

public class Dispatch
{
    [YamlMember(Alias = "workflow_dispatch")]
    public WorkflowDispatch WorkflowDispatch { get; set; }
}

public class WorkflowDispatch
{
    [YamlMember(Alias = "inputs")]
    public Input[] Inputs { get; set; }
}

public class Input
{
    [YamlMember(Alias = "key")]
    public string Key { get; set; }

    [YamlMember(Alias = "description")]
    public string Description { get; set; }

    [YamlMember(Alias = "required")]
    public bool Required { get; set; }

    [YamlMember(Alias = "type")]
    public string Type { get; set; }
}

public class InputObjectNodeDeserializer : INodeDeserializer
{
    public bool Deserialize(IParser reader, Type expectedType, Func<IParser, Type, object?> nestedObjectDeserializer, out object? value)
    {
        if (expectedType != typeof(Input[]))
        {
            value = null;
            return false;
        }

        if (!reader.Accept<MappingStart>())
        {
            value = null;
            return false;
        }

        reader.Consume<MappingStart>();
        var result = new List<Input>();
        while (!reader.TryConsume<MappingEnd>(out var _))
        {
            var keyScalar = reader.Consume<Scalar>();
            var input = nestedObjectDeserializer(reader, typeof(Input)) as Input;
            if (input != null)
            {
                input.Key = keyScalar.Value;
            }
            result.Add(input);
        }

        value = result.ToArray();
        return true;
    }
}

The output

reason - The reason for the manual run
something_else - Some other input

I'm going to close this issue since this is an example of doing what you want, re-open if you need serialization too.

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

3 participants