NeVeSpl / NTypewriter

File/code generator using Scriban text templates populated with C# code metadata from Roslyn API.
https://nevespl.github.io/NTypewriter/
MIT License
126 stars 25 forks source link

Select classes with a specific attribute? #8

Closed gregveres closed 3 years ago

gregveres commented 3 years ago

Hi,

I just stumbled upon your project. It looks really interesting. I have been a Typewriter user for 5 years.

I have a code base that uses an attribute called ExportToTypescript to indicate in my c# code which classes and enums should be converted to TS through Typewriter.

I have been looking through your unit tests and that has helped me understand Scriban a bit. I have also been reading the Scriban github docs to understand the language.

But I am confused on the very highest level structure of the .nt file. I can see how I can just start writing output and then do substitution within that output using snippets of scriban. But what I want as output is one file per enum and one file per class that are tagged with the attribute. This is where I get confused on the strucutre of the scriban file.

I think I need to first filter the list with something like this:

for class in data.Enums | Symbols.ThatHaveAttribute "ExportToTypescript"

But do I put this in {{ }} and then use String.Append for all of my output? Something like this:

{{  
  for enum in data.Enums | Symbols.ThatHaveAttribute "ExportToTypescript" 
    capture output
      "export const enum " | String.Append enum.Name | String.Append " {\r\n"
      ...
   end
   Save output (enum.BareName | String.Append ".ts")

or is there away to have the text just appear in my output without having to use String.Append?

Thanks

gregveres commented 3 years ago

I have done some more experimenting. I guess the other approach would be like this, right?

{{ for enum in data.Enums  | Symbols.ThatHaveAttribute "ExportToTypescript" }}
{{capture output}}export const enum {{enum.Name}} {
    ...
}
{{end}}
{{Save output (enum.BareName | String.Append ".ts")}}
{{end}}

Is there a preference in terms of which one is best practice?

gregveres commented 3 years ago

Here is what I have come up with for output Enums that are tagged with an attribute called ExportToTypescript. I am putting it here to help future people get started.

{{- for enum in data.Enums  | Symbols.ThatHaveAttribute "ExportToTypescript" -}}
{{- capture output -}}
export const enum {{enum.Name}} {
  {{- for enumValue in enum.Values}}
  {{ enumValue.Name}} = {{ enumValue.Value}}{{-if !for.last}},{{end}}
  {{-end}}
}
{{- end}}
{{- Save output (enum.BareName | String.Append ".ts")}}
{{- end}}

This will produce a file like this:

export const enum AvailabilityStatus {
  Available = 0,
  NotAvailable = 1,
  IfNeeded = 2
}

For beginners (like me) these are the things to note:

gregveres commented 3 years ago

And here is my finished .nt file that creates Enum definitions in an Enums folder and Interface definitions in an Interfaces folder. I am putting this here as an example of a non-trivial, fully functioning example for future people to learn from. I think it serves as a pretty good example, but since I just learned Scriban this morning, I am sure there are better ways to do some of the things I am doing.

My next step is to create functions that I can call in TS that will call my controller end points. BTW, this is much simpler than the script I had in Typewritter

{{- # Helper classes }}
{{- importedTypeNames = []}}
{{- func ImportType(type)
    useType = Type.Unwrap(type)
    if (useType.ArrayType != null) 
        useType = useType.ArrayType
    end
    if (importedTypeNames | Array.Contains useType.Name) 
        ret null
    end
    importedTypeNames[importedTypeNames.size] = useType.Name
    if (type.IsEnum) 
        path = "../Enums/"
    else
        path = "./"
    end
    importType = false
    for attr in useType.Attributes
        if attr.Name == "ExportToTypescript"
    importType = true
    break
        end
        if attr.Name == "ExportToTypescriptWithKnockout"
    importType = true
    break
        end
    end
    if importType
    ret "import { " | String.Append useType.Name | String.Append " } from '" | String.Append path | String.Append useType.Name | String.Append "';\r"
    end
    ret null
    end
}}
{{- func ToTypeScriptType(type)
    if(type.Name | String.Contains "TimeSpan")
        ret "string"
    else if (type.Name == "DateTime")
        ret "string"
    else if (type.Name == "DateTimeOffset")
        ret "string"
    end
    ret Type.ToTypeScriptType type
    end
}}

{{- # output enums }}
{{- for enum in data.Enums  | Symbols.ThatHaveAttribute "ExportToTypescript" -}}
{{- capture output -}}
export const enum {{enum.Name}} {
  {{- for enumValue in enum.Values}}
  {{ enumValue.Name}} = {{ enumValue.Value}}{{-if !for.last}},{{end}}
  {{-end}}
}
{{- end}}
{{- Save output ("Enums\\" | String.Append enum.BareName | String.Append ".ts")}}
{{- end}}

{{- # output classes }}
{{ normal = data.Classes | Symbols.ThatHaveAttribute "ExportToTypescript"
   knockout = data.Classes | Symbols.ThatHaveAttribute "ExportToTypescriptWithKnockout"
   classes = Array.Concat normal knockout
}}
{{- for class in classes  -}}
{{- capture output -}}
  {{- importedTypeNames = []}}
  {{- if class.HasBaseClass
        ImportType class.BaseClass
      end
  }}
  {{-for prop in class.Properties}}
    {{- ImportType prop.Type}}
  {{-end}}

export interface {{class.Name}}{{if class.HasBaseClass}} extends {{class.BaseClass.Name}}{{end}} {
  {{- for prop in class.Properties | Symbols.ThatArePublic }}
  {{ prop.Name | String.ToCamelCase}}: {{ToTypeScriptType prop.Type}}{{if !for.last}},{{end}}
  {{-end}}
}
{{- end}}
{{- Save output ("Interfaces\\" | String.Append class.BareName | String.Append ".ts")}}
{{- end}}
NeVeSpl commented 3 years ago

Have you tried to use Type.AllReferencedTypes (TypeFunctions.AllReferencedTypes.cs)? It should streamline further generating import part and reduce size of your template by a few lines. There was a bug in version 0.0.6, arrays were not handled properly, but it was fixed in 0.0.7.

gregveres commented 3 years ago

Ah, I see, I can replace this section of the output classes section:

  {{- importedTypeNames = []}}
  {{- if class.HasBaseClass
        ImportType class.BaseClass
      end
  }}
  {{-for prop in class.Properties}}
    {{- ImportType prop.Type}}
  {{-end}}

with

{{- for type in Type.AllReferencedTypes class}}
  {{- ImportType type}}
{{-end}}

because AllReferencedTypes will return the base class and all the classes associated with the methods and properties etc. And it will return a unique list, so I don't have to build up a unique list myself. Nice.