Geta / geta-optimizely-genericlinks

An extensive alternative to LinkItemCollection in Optimizely.
Apache License 2.0
4 stars 0 forks source link

Deserialisation of LinkDataCollection in Optimizely v12 ContentManagement API #14

Closed antonysmith-mando closed 1 year ago

antonysmith-mando commented 1 year ago

Hello,

I'm an Optimizely newbie, so apologies if this is a silly question.

I'm creating items in Optimizely v12 via the Content Management API.

I've got a property of type LinkDataCollection<GeneralLinkData> which all works fine in the CMS and via GET requests to the Content Management API.

Retrieving a Block with this property contains the following JSON:

    "links": {
        "value": [
            {
                "target": "_self",
                "title": "Test Link 1",
                "url": {
                    "originalString": "https://www.google.co.uk",
                    "path": "/",
                    "authority": "www.google.co.uk",
                    "dnsSafeHost": "www.google.co.uk",
                    "fragment": "",
                    "host": "www.google.co.uk",
                    "isAbsoluteUri": true,
                    "localPath": "/",
                    "pathAndQuery": "/",
                    "port": 443,
                    "query": "",
                    "queryCollection": [],
                    "scheme": "https",
                    "segments": [
                        "/"
                    ],
                    "userEscaped": false,
                    "userInfo": "",
                    "uri": "https://www.google.co.uk",
                    "encoding": {
                        "bodyName": "utf-8",
                        "encodingName": "Unicode (UTF-8)",
                        "headerName": "utf-8",
                        "webName": "utf-8",
                        "windowsCodePage": 1200,
                        "isBrowserDisplay": true,
                        "isBrowserSave": true,
                        "isMailNewsDisplay": true,
                        "isMailNewsSave": true,
                        "isSingleByte": false,
                        "encoderFallback": {
                            "defaultString": "�",
                            "maxCharCount": 1
                        },
                        "decoderFallback": {
                            "defaultString": "�",
                            "maxCharCount": 1
                        },
                        "isReadOnly": true,
                        "codePage": 65001
                    }
                },
                "openInNewTab": false,
                "href": "https://www.google.co.uk",
                "text": "Test Link 1",
                "attributes": {
                    "openInNewTab": "false",
                    "href": "https://www.google.co.uk"
                }
            },
            {
                "target": "_self",
                "title": "Test Link 2",
                "url": {
                    "originalString": "https://www.google.co.uk",
                    "path": "/",
                    "authority": "www.google.co.uk",
                    "dnsSafeHost": "www.google.co.uk",
                    "fragment": "",
                    "host": "www.google.co.uk",
                    "isAbsoluteUri": true,
                    "localPath": "/",
                    "pathAndQuery": "/",
                    "port": 443,
                    "query": "",
                    "queryCollection": [],
                    "scheme": "https",
                    "segments": [
                        "/"
                    ],
                    "userEscaped": false,
                    "userInfo": "",
                    "uri": "https://www.google.co.uk",
                    "encoding": {
                        "bodyName": "utf-8",
                        "encodingName": "Unicode (UTF-8)",
                        "headerName": "utf-8",
                        "webName": "utf-8",
                        "windowsCodePage": 1200,
                        "isBrowserDisplay": true,
                        "isBrowserSave": true,
                        "isMailNewsDisplay": true,
                        "isMailNewsSave": true,
                        "isSingleByte": false,
                        "encoderFallback": {
                            "defaultString": "�",
                            "maxCharCount": 1
                        },
                        "decoderFallback": {
                            "defaultString": "�",
                            "maxCharCount": 1
                        },
                        "isReadOnly": true,
                        "codePage": 65001
                    }
                },
                "openInNewTab": false,
                "href": "https://www.google.co.uk",
                "text": "Test Link 2",
                "attributes": {
                    "openInNewTab": "false",
                    "href": "https://www.google.co.uk"
                }
            }
        ],
        "propertyDataType": "GeneralLinkCollection"
    }

However, if I try to create a new Block using exactly the same JSON, I get the following error:

Cannot deserialize the current JSON array (e.g. [1,2,3]) into type 'Geta.Optimizely.GenericLinks.LinkDataCollection' because the type requires a JSON object (e.g. {\"name\":\"value\"}) to deserialize correctly.\r\nTo fix this error either change the JSON to a JSON object (e.g. {\"name\":\"value\"}) or change the deserialized type to an array or a type that implements a collection interface (e.g. ICollection, IList) like List that can be deserialized from a JSON array. JsonArrayAttribute can also be added to the type to force it to deserialize from a JSON array.\r\nPath 'value'.

Is there a step I'm missing to allow this object to be deserialised via the Content Management API?

I see there's a separate package for performing conversions for the Content Delivery API (referenced in #6) but not sure if this will solve my problems in the Content Management API and thought I'd ask before throwing myself down a(nother!) rabbit hole.

Any help would be greatly appreciated.

Thanks,

Antony

svenrog commented 1 year ago

I think one of the issues here is that the underlying LinkDataCollection isn't based on JSON at all. It's based on XML and without the proper converters native serialization will fail.

The Content Management API is dependent on the Content Delivery API. Follow the configuration guide here.

This should leave you with an output akin to this:

"thumbnailLinks": [
{
    "thumbnail": "62",
    "text": "Text",
    "title": null,
    "target": "_blank",
    "href": "/en/alloy-track/",
    "attributes": {
        "thumbnail": "62",
        "href": "/en/alloy-track/",
        "target": "_blank"
    }
},
...
]
lanorkin commented 1 year ago

I've had similar issues, so have spent some time trying to make it work for my case (everything below is specific for Content Management API - as Content Delivery API seems to work OK).

Sorry I don't have a solution in the form of pull request, as it might require some rethinking of how to use generic approach to work with Content Management API.

Based on docs here https://docs.developers.optimizely.com/content-cloud/v1.6.0-content-management-api/docs/deserialization

Things I see so far:

1 - Content Management API won't use generic properties (some digging with dotpeek led to this).

So if we have say CustomLinkData, LinkDataCollection<CustomLinkData>, and a PropertyCustomLinkDataCollection for it - we will need a separate model like this CustomLinkDataCollectionPropertyModel : PropertyModel<LinkDataCollection<CustomLinkData>, PropertyCustomLinkDataCollection> - i.e. we cannot inherit from GenericLinkCollectionPropertyModel<T>.

2 - This Model should have parameterless constructor, to support JSON deserialization.

3 - And we need yet another converter - this time to implement IPropertyDataValueConverter.

Here is a minimal sample (in site custom code) I was able to do to make it work with Content Management API and current version of Geta Generic Links:

public class CustomLinkData : LinkData
{
}

[Serializable]
[PropertyDefinitionTypePlugIn]
public class PropertyCustomLinkData : PropertyLinkData<CustomLinkData>
{
}

[Serializable]
[PropertyDefinitionTypePlugIn]
public class PropertyCustomLinkDataCollection : PropertyLinkDataCollection<CustomLinkData>
{
}

public class CustomLinkDataCollectionPropertyModel : PropertyModel<LinkDataCollection<CustomLinkData>, PropertyCustomLinkDataCollection>, IExpandableProperty<LinkDataCollection<CustomLinkData>>
{
    // Inner property we're wrapping here
    private GenericLinkCollectionPropertyModel<CustomLinkData> InnerProperty { get; }

    // We need default constructor to support deserialize from JSON during Content Management API POST requests
    [JsonConstructor]
    public CustomLinkDataCollectionPropertyModel() : this(new PropertyCustomLinkDataCollection())
    {
    }

    public CustomLinkDataCollectionPropertyModel(PropertyCustomLinkDataCollection type) : base(type)
    {
        InnerProperty = new GenericLinkCollectionPropertyModel<CustomLinkData>(type, ServiceLocator.Current.GetInstance<IUrlResolver>());
        Value = InnerProperty.Value;
    }

    public virtual LinkDataCollection<CustomLinkData> ExpandedValue
    {
        get => InnerProperty.ExpandedValue;
        set => InnerProperty.ExpandedValue = value;
    }

    public virtual void Expand(CultureInfo language)
    {
        InnerProperty.Expand(language);
    }
}

[ServiceConfiguration(typeof(IPropertyDataValueConverter))]
[PropertyDataValueConverter(new Type[] { typeof(CustomLinkDataCollectionPropertyModel) })]
internal class CustomLinkDataCollectionPropertyDataValueConverter : IPropertyDataValueConverter
{
    public object Convert(IPropertyModel propertyModel, PropertyData propertyData)
    {
        if (propertyModel is null)
        {
            throw new ArgumentNullException(nameof(propertyModel));
        }

        return ((CustomLinkDataCollectionPropertyModel)propertyModel).Value;
    }
}
svenrog commented 1 year ago

Thanks for the feedback. I'll have a look in the coming days.

svenrog commented 1 year ago

I've (finally) concluded that this logic currently has to be implemented on the user side like your example @lanorkin.

The reason for this being how Optimizely implemented registration of IPropertyDataValueConverterResolver inside an internal namespace. Attaching behaviour to that logic would be brittle and hard to support.

svenrog commented 1 year ago

Closing due to inactivity.