aosasona / gots

No matter how you pronounce it, this repository is all about converting Go types to TypeScript types.
MIT License
19 stars 0 forks source link

Re-usablity - Complex Primitives - Type Composition - Type Inheritance #3

Closed opensaucerer closed 1 year ago

opensaucerer commented 1 year ago

There's something I like about you, it's that you don't write software like most people I've met...this means you don't do the boring and what I crudely call "shallow" programming other backend engineers engage in...just building servers/APIs (bro...can we just stop ffs...lol). Not saying it's bad or clowning down on anyone...rather just hoping we'll have more people engage in more exciting software stuff as opposed to spending time arguing over FE/BE narrative.

Now, gots is awesome and I like the naming you use for your creations as well...I think we have these parts in common and we might just end up writing software together...maybe, maybe not...but here's the problem statement for this issue.

  1. After a call to gots.New() the client returned can only be used once for a single file which means I have to create a new client for another file. I think one can make it such that gots.New() still takes the default OutputFile parameter but allows the returned client to implement a function that allows for new output. Something like gots.New().Register().Output() or a cooler name.

  2. I call them complex primitives because they are basic types in Golang that do not exist in Typescript as it inherits from JavaScript's lack of granular support for large integers. I say granular because we have BigInt.

Consider the following...you can easily see that the out from gots will be a single call to toSingleType which results in a Typescript error.

package main

import (
    "log"
    "time"

    "github.com/aosasona/gots"
)

func main() {
    type Integer int32

    var integer Integer

    ts := gots.New(gots.Config{
        Enabled:           true,               // you can use this to disable generation
        OutputFile:        "./cmd/index.d.ts", // this is where your generated file will be saved
        UseTypeForObjects: false,              // if you want to use `type X = ...` instead of `interface X ...`
    })

    // registering multiple types at once
    err := ts.Register(integer)
    if err != nil {
        log.Fatalf("error: %s\n", err.Error())
    }
}
  1. Golang has not inheritance but it does exhibit the behavior, in a way, using composition. I dislike inheritance by the way. The sample code in gots's readme makes use of some structs with fields extending other struct types but a modification as follows will produce not an error but an undesired result.
package main

import (
    "log"
    "time"

    "github.com/aosasona/gots"
)

func main() {
    type Profession string

    type Person struct {
        firstName  string `ts:"name:first_name"`
        lastName   string `ts:"name:last_name"`
        dob        string
        profession Profession `ts:"name:job,optional:true"`
        createdAt  time.Time
        isActive   bool `ts:"name:is_active"`
    }

    type Collection struct {
        collectionName string   `ts:"name:name"`
        people         []Person // an array of another struct
        Person  // notice that this is a composition (it's like doing `interface Collection extends Person`
    }

    c := Collection{
        Person: Person{
            firstName: "John",
        },
    }

    c.firstName = "John" // I can directly access the fields available on person but `gots` now will make it such that I have to do c.Person.frist_name instead of c.first_name

    ts := gots.New(gots.Config{
        Enabled:           true,               // you can use this to disable generation
        OutputFile:        "./cmd/index.d.ts", // this is where your generated file will be saved
        UseTypeForObjects: false,              // if you want to use `type X = ...` instead of `interface X ...`
    })

    // registering multiple types at once
    err := ts.Register(*new(Profession), Person{}, Collection{}, integer)
    if err != nil {
        log.Fatalf("error: %s\n", err.Error())
    }
}

Awesome package, once again.

aosasona commented 1 year ago

Thanks a lot man! Perhaps will work together some time haha. And I don't exactly use the term 'backend developer' for myself; software engineer just works 😄 - I write whatever needs to be written and if i can't; well, I learn to.

  1. An instance was intentionally made to not be able to save in different files to keep things simple. Having one instance that spits out things into multiple files would become confusing fast in a large codebase, the idea is to be able to pass that one instance around and know EXACTLY where those types end up from a single source of truth. But that being said, I could easily implement that method in a future release, I will create an issue for that; not entirely convinced I should give users that option because it'll technically just create another instance but I will anyway.
  2. Yeah, that was my bad, I wasn't using the mapped type 😆
  3. You are right, I do know about this. This is one of the many things I hate about Golang (the useless generics feat is higher on the list) but here's a good example why this is sort of thing is hard to resolve because then it comes down to "What is the truth?"
package main

type Foo struct {
    Name string
    Age  int
}

type Bar struct {
    Foo
    Age int
}

func main() {
    foo := Foo{"foo", 10}
    bar := Bar{foo, 20}
    println(bar.Age)     // 20
    println(bar.Foo.Age) // 10
}

Here, what exactly is the real age?🤣 I could tweak it to always just merge the types the way you seem to suggest but it might be unpleasant. I get that the direct definition overrides the embedded one but imagine they're different types now and both now get combined into:

{
...
age: string;
age: number;
...
}

I apologize but I am honestly unsure how to go about this one in a way that it spits out accurate Typescript that will ALWAYS match whatever you do in code, if you know how to possibly resolve this in a way that allows for both cases, please make a PR, I'll gladly review and merge, I am out of ideas for this one.

opensaucerer commented 1 year ago
aosasona commented 1 year ago

Whether you're chaining on to a client or creating a new one, it's going to do the same thing; return a pointer to a new gots instance, something you could easily do from anywhere in your code by just creating a new one, don't see the efficiency hit or boost, but I'll add that feat in a future release.

opensaucerer commented 1 year ago

Hun? a new instance or the same instance? Wondering why it has to be a new instance? Maybe I'm missing something?

opensaucerer commented 1 year ago
func New(config Config) *gots {
    if config.OutputFile == "" {
        config.OutputFile = "index.d.ts"
    }

    return &gots{
        config,
    }
}

func (g *gots) Reconfigure(config Config) error {
        if config.OutputFile == "" {
        config.OutputFile = "index.d.ts"
    }

        // to ensure the fields are dynamically handled, use reflect to scan through the fields an load them unto the g pointer that way you can still return the same instance...referencing the same space in memory.

    return g
}
opensaucerer commented 1 year ago

Do correct me if I'm wrong, surely.

aosasona commented 1 year ago

The same instance just gives you the same exact configurations as the original instance, and modifying that instance just changes the configuration globally, what’s the point then?

I would advice you take a look at this to better understand: https://github.com/aosasona/gots/blob/fa3d9f02370589f124706fde784a145d7aad2551/file.go#L10

The output file is grabbed from the config itself no matter where or how many times you call register, changing the same instance just changes that for everything else.

aosasona commented 1 year ago
func New(config Config) *gots {
  if config.OutputFile == "" {
      config.OutputFile = "index.d.ts"
  }

  return &gots{
      config,
  }
}

func (g *gots) Reconfigure(config Config) error {
        if config.OutputFile == "" {
      config.OutputFile = "index.d.ts"
  }

        // to ensure the fields are dynamically handled, use reflect to scan through the fields an load them unto the g pointer that way you can still return the same instance...referencing the same space in memory.

  return g
}

Then you’re just changing it globally? Still one output file, your pitch was multiple output files.

opensaucerer commented 1 year ago

You're correct...what I was actually pointing at is the global change...wasn't referring to a local scope.

opensaucerer commented 1 year ago

Like, you give me a gots client, I set the output file to something I need at this moment, I use it to call register and it compile one file and that's the end. I don't need to create another instance that'll need to be gc'ed when done.

I know my instance is still available to me but since i know I want a different output path, I just reconfigure it and use it again for the set of types I want...on and on for different output path but the same client.

opensaucerer commented 1 year ago

and then I don't need the client anymore...I can call g.Close() but this one might not be necessary. I think it's the most idiomatic way I've seen go project clients be implemented in, if I'm not wrong.

say for example...I create a google cloud storage client that only operates on one bucket...and have to create another for another bucket.

don't know if you do get me, my lord.

aosasona commented 1 year ago

Would suggest you go through the code again to understand how it works but then again, I have created an issue to branch off from an instance, will work on that in the future.

And it’s not something you use in production, I’d expect you keep it in one place, it’s like suddenly modifying your database instance in one file out of 100 when you could do it in whatever init function you created it in.

aosasona commented 1 year ago

Would suggest you take a look at this package if you need something more fully fledged; https://github.com/tkrajina/typescriptify-golang-structs

opensaucerer commented 1 year ago

Yeah...I saw the production advice in the readme...and I did read the code before even opening this issue.

Then when I tried the package...I wanted to use it in exporting another struct to another location. it was then I realized I needed to create a new client...then I thought to suggest...that if this is the first problem I face trying out a cool package, it might as well be something others might have thought of or faced.

So, I just checked the link you shared above and I saw this which is almost like what I'm mentioning if not the exact same.

converter := typescriptify.New().
    Add(Person{}).
    Add(Dummy{})
err := converter.ConvertToFile("ts/models.ts")
if err != nil {
    panic(err.Error())
}

and, just pointing out that I'm not actually saying this should be done now or anything...just loved the idea of the project and some others I've seen you work on.

have you worked on building a web framework before in any language? I think you're still a student..I'm still a student too..lol...see me talking like I'm done with school...but I think you're outside the country....I don't know if your wizardry will be interested in the idea of this framework...it's not just any regular one...it's BARF - (Basically, A Remarkable Framework) and the name BARF is from marvel's reference, Binarily Augumented Retro Framing....you know it?

I also have an idea for a validation layer, a struct validation layer like that of Joi in JavaScript. I call the package Vibranium - probably the most versatile go validation package.

EDIT:

I just read this reply again and I realized there's absolutely no reference to why I asked if you're a student. I asked due to availability.

aosasona commented 1 year ago

It appears I have confused you, my bad. Let me try to re-explain to you again.

aosasona commented 1 year ago

First, I need you to consider what happens if you call ConvertToFile in main.go and then proceed to do .Add() in another file. You might be missing the point here because of how I have explained things.

This .Add().Add() is the same as passing it all at once as I have done with Register which ensures: it does no work outside that one call, and nothing is left open as you implied. My mistake was not including a way to branch off from a global client and when I do that, you will be able to do this:

modelClient := helper.GotsClient.To("./somefile.ts") // assuming you have created a global client in the helper package called GotsClient

// then you can give that one a  new config or let it inherit from the original one (which means it will respect your Enable toggle in one place (you dig?)

modelClient.SetEnable(...)
modelClient.SetUseTypesForObject(...)
// or do it at once
modelClient.SetOpts(gots.Config{...})

The problem with being able to call register anywhere and then another SaveToFile method is that; it'll always be a problem, there is a very good chance that no matter where you call SaveToFile, there will be one or more unregistered struct. Without looking at the code, I can tell that the way this works is by appending each struct to a slice (I just make you pass in the structs directly) and working on that slice when you call the Convert thingy; which was my initial idea but you then have to worry about "Has all my types been registered yet?", "Where should I call Save to make sure they all get registered?" etc

aosasona commented 1 year ago

Yeah, I have seen the Vibranium package you're working on, I follow you, so it showed up in my feed when you created the repo 🤣 - I have been waiting for the release, would also have to use that your multi-storage package for a project I am working on soon; expect issues to when I do 😈

A framework; yeah, thought about it for PHP but it's probably going to suck more than anything else already on the market since I won't really have the time to actually implement things well or maintain it. I just use Validator for validation and it hasn't pissed me off enough to warrant reinventing it

opensaucerer commented 1 year ago

Oh...that's correct....Add() will simply append to a slice...that's the most reasonable way to do it without expending much memory but, of course, with some irrelevant drawbacks to gc not triggering even when you nil the array within the calling function (backing array ish).

I actually like the way you let your Register function take variadic inputs... I don't like the builder design pattern used by this other package... I referenced that snipped because I wanted to point your attention the .ConvertToFile() method and from your last comment, it seems that's the same thing you picked up from the snippet.

Now, regarding your edit to your comment just now about worrying if all structs are registered...I believe this is where you can then make it such that well, the builder pattern comes in, in a way...really wish I can use emoji directly here...cos I'm just smiling seeing that the project is getting more involved and I don't think you want that...but, yeah...the builder pattern comes in-ish such that I can call Register more than one to append another set of structs before calling the .To() method...because after .To() gets called...every registered structs should get disposed of.

Essentially, gots might have to become more involved than you initially thought and you don't have to much it such, if not necessary but because I like learning and talking about exciting things with people, I just opted to talk about this one.

opensaucerer commented 1 year ago

Yeah, I have seen the Vibranium package you're working on, I follow you, so it showed up in my feed when you created the repo rofl - I have been waiting for the release, would also have to use that your multi-storage package for a project I am working on soon; expect issues to when I do smiling_imp

A framework; yeah, thought about it for PHP but it's probably going to suck more than anything else already on the market since I won't really have the time to actually implement things well or maintain it. I just use Validator for validation and it hasn't pissed me off enough to warrant reinventing it

Looking forward to you using it and I'll be expecting issues...I love issues 😂 (had to copy this emoji and paste here...dang!)

Now that I'm sorta out of work...I'll take all the time to just build my ideas... all of them reside here https://abbrefy.xyz/projects

Exactly why I'm opting to do a framework in golang. one, I don't like frameworks just as much as I don't like inheritance...i think they end up making things complex...I see you use fiber...I'm not much of a fan of any of them...I just use gorilla mux to do stuff and write everything else myself.

But the idea of BARF is to make it such that you can build backend servers without having to actually create any instance of the app client itself. Like say in express const app = Express() or in fibre app := Fiber.New()...in barf, you don't need to create a client...just access everything directly from the package itself name itself barf.Get(), barf.Router().Get() etc. and when you want to expose the port...barf.Beck(), for custom configurations, barf.Stark()

I've started and I'll push in a few days, my progress.

Then later on, barf will become such that REST is not supported by default rather you have to enable the support for rest in the configuration to barf.Stark().....by the way, Stark() as in Tony Stark and Beck() as in Quitine Beck 😂