Zaid-Ajaj / Hawaii

dotnet CLI tool to generate type-safe F# and Fable clients from OpenAPI/Swagger or OData services
MIT License
140 stars 15 forks source link

Question: Why does my generated type have a member of type `string option` instead of `ActualType option`? #19

Closed travis-leith closed 3 years ago

travis-leith commented 3 years ago

I generated with the schema: "https://selectapi.datascope.refinitiv.com/RestApi/v1/Authentication/$metadata".

One of the types that is generated is

type ActionImportRequestTokenPayload =
    { Credentials: Option<string> }

but it should be

type ActionImportRequestTokenPayload =
    { Credentials: Option<DataScopeSelectApiAuthenticationCredentials> }

Looking at the --from-odata-schema output, I see a few cases of anyOf, and I see from the readme section that anyOf is not supported.

Is the current absence of support the reason these types get represented as strings?

Zaid-Ajaj commented 3 years ago

Is the current absence of support the reason these types get represented as strings?

That's right, when a property can't be resolved to anything known, it defaults to string. In case of these anyOf types, I think it should be possible to fix it when there is only one element in the anyOf definition and reduce the anyOf away from there

i.e. { anyOf: [ someType ] } gets reduced to { ...someType }

Will give it a try sometime soon

Zaid-Ajaj commented 3 years ago

Fixed as of Hawaii v0.42 🚀 can you please give it a try? Let me know how it goes 😄

travis-leith commented 3 years ago

works, thank you.

travis-leith commented 3 years ago

I think I have found an opportunity for further improvement on this idea. I have encountered the following

"IdentifierList": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/DataScope.Select.Api.Extractions.ExtractionRequests.SubjectIdentifierList"
                  }
                ],
                "nullable": true
              },

which gets represented as string option. Would it be possible to extend what you have done to cater for anyOf lists of a single type?

Zaid-Ajaj commented 3 years ago

Hmm can you please elaborate on this issue because that is exactly the case that I fixed previously: anyOf/oneOf a single type gets resolved into that type 🤔

If you can provide me with a schema to test with, it would be great!

travis-leith commented 3 years ago

The schema I am currently looking at is "https://selectapi.datascope.refinitiv.com/RestApi/v1/Extractions/$metadata". It is a very large schema unfortunately.

One of the difficulties seem to be the use of the following empty entity

"DataScope.Select.Api.Extractions.ExtractionRequests.ExtractionRequestBase": {
        "title": "ExtractionRequestBase",
        "type": "object",
        "description": "Represents an on demand extraction request."
      },

as part of some sort of inheritance setup or something, which results in the following generated type

type ActionImportExtractPayload =
    { ///Represents an on demand extraction request.
      ExtractionRequest: Option<Newtonsoft.Json.Linq.JObject> }
    ///Creates an instance of ActionImportExtractPayload with all optional fields initialized to None. The required fields are parameters of this function
    static member Create (): ActionImportExtractPayload = { ExtractionRequest = None }

We also have the following

"DataScope.Select.Api.Extractions.ExtractionRequests.TermsAndConditionsExtractionRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/DataScope.Select.Api.Extractions.ExtractionRequests.ExtractionRequestBase"
          },
          {
            "title": "TermsAndConditionsExtractionRequest",
            "type": "object",
            "properties": {
              "ContentFieldNames": {
                "type": "array",
                "items": {
                  "type": "string",
                  "nullable": true
                }
              },
              "IdentifierList": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/DataScope.Select.Api.Extractions.ExtractionRequests.SubjectIdentifierList"
                  }
                ],
                "nullable": true
              },
              "Condition": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/DataScope.Select.Api.Extractions.ReportTemplates.TermsAndConditionsCondition"
                  }
                ],
                "nullable": true
              }
            },
            "description": "Extraction request for Reference Data: Terms and Conditions"
          }
        ]
      }

which results in the following generated type, where identifier list has now become a string.

type DataScopeSelectApiExtractionsExtractionRequestsTermsAndConditionsExtractionRequest =
    { ContentFieldNames: Option<list<string>>
      IdentifierList: Option<string>
      Condition: Option<string> }
    ///Creates an instance of DataScopeSelectApiExtractionsExtractionRequestsTermsAndConditionsExtractionRequest with all optional fields initialized to None. The required fields are parameters of this function
    static member Create (): DataScopeSelectApiExtractionsExtractionRequestsTermsAndConditionsExtractionRequest =
        { ContentFieldNames = None
          IdentifierList = None
          Condition = None }

and now that I have written this all out I see that it is because of the same issue (which I think is related to inheritance)

      "DataScope.Select.Api.Extractions.ExtractionRequests.SubjectIdentifierList": {},
      "DataScope.Select.Api.Extractions.ExtractionRequests.EntityIdentifierListBase": {},
      "DataScope.Select.Api.Extractions.ExtractionRequests.EntityIdentifierList": {
        "value": {
          "EntityIdentifiers": [
            {
              "@odata.type": "DataScope.Select.Api.Content.EntityIdentifier"
            }
          ]
        }
      }
travis-leith commented 3 years ago

If this is solvable, I expect that it might involve the generation of marker interfaces or an abstract class. But I see no way of inferring that EntityIdentifierList inherits from SubjectIdentifierList, which I am guessing it in fact does.

Zaid-Ajaj commented 3 years ago

@travis-leith I will take a look. Empty object definitions are a bit weird to work with but there is an option to generate a type alias from it:

"empty definitions": "free-form"

Can you try it out?

As for IdentifierList being resolved to option, that sounds like another problem 🤔 just in case if you update Hawaii to latest v0.44 it might solve the problem but it's a shot in the dark

travis-leith commented 3 years ago

After upgrading to 0.44, and setting free-form, I get the following alias

type DataScopeSelectApiExtractionsExtractionRequestsSubjectIdentifierList = Newtonsoft.Json.Linq.JToken

but that alias is not used in

type DataScopeSelectApiExtractionsExtractionRequestsTermsAndConditionsExtractionRequest =
    { ContentFieldNames: Option<list<string>>
      IdentifierList: Option<string>
      Condition: Option<string> }
    ///Creates an instance of DataScopeSelectApiExtractionsExtractionRequestsTermsAndConditionsExtractionRequest with all optional fields initialized to None. The required fields are parameters of this function
    static member Create (): DataScopeSelectApiExtractionsExtractionRequestsTermsAndConditionsExtractionRequest =
        { ContentFieldNames = None
          IdentifierList = None
          Condition = None }
Zaid-Ajaj commented 3 years ago

I think I have solved the issue now as of v0.45 which expands the schema reduction. Can you give it try again?

Zaid-Ajaj commented 3 years ago

One thing that I really dislike right now with odata code-gen is the really long type names, not sure how to tackle that in a reliable way

travis-leith commented 3 years ago

I think I have solved the issue now as of v0.45 which expands the schema reduction. Can you give it try again?

I am now getting a JToken instead of a string, so looks like it is working.

One thing that I really dislike right now with odata code-gen is the really long type names, not sure how to tackle that in a reliable way

Given the spec DataScope.Select.Api.Extractions.ExtractionRequests.TermsAndConditionsExtractionRequest ...., would it be possible to represent it in a nested module structure, such as

module DataScope =
    module select =
        module Api =
            module Extractions =
                module ExtractionRequests =
                    type TermsAndConditionsRequest =

some adjustments to how the namesspaces work would need to be made.

Zaid-Ajaj commented 3 years ago

Yeah that makes really complicated (even more than they are right now)

travis-leith commented 3 years ago

What gets more complicated, the generated code, or the generating code, or both? I realize it is one thing to propose 'solutions', but an entirely more difficult thing to implement them, however:

How about you name it from the last dot : TermsAndConditionsExtractionRequest and if there is a conflict then you go up a level to

module ExtractionsRequests =
    type TermsAndConditionsExtractionRequest

This is basically what I am doing with my manually edited version of the generated code.

Zaid-Ajaj commented 3 years ago

Generating the code becomes a lot more difficult, but will think about it nonetheless. Using type aliases should be easy enough for consumers now