badcafe / jsonizer

Easy nested instance reviving for JSON
MIT License
9 stars 0 forks source link

Request for new "discriminator" joker for polymorphic array #1

Open justinkwaugh opened 6 months ago

justinkwaugh commented 6 months ago

Hello,

I've been trying out this library and I really like how it is not intrusive to the classes. Unfortunately I have a situation where I have a generic class with a polymorphic array that I would like to revive based on a reviver defined on an extending class and I'm not sure how to accomplish that. But it seems like there is a nice way which could be added... here is a contrived example of the situation:

interface Prize {
   readonly type: string
}

@Reviver<Ball>({ 
    '.': Jsonizer.Self.assign(Ball)
})
class Ball implements Prize {
   readonly type: string = 'ball'
   color: string

   constructor(color:string) {
      this.color = color
   }

   bounce() {
      // some impl
   }
}

@Reviver<Gum>({ 
    '.': Jsonizer.Self.assign(Gum)
})
class Gum implements Prize {
   readonly type: string = 'monkey'
   numSticks: number

   constructor(numSticks:number) {
      this.numSticks = numSticks
   }

   chew() {
      // some impl
   }
}

class DrawBag<T> {
   items:T[]
   remaining: number

   constructor(items:T[]) {
      this.items = items
   }

   draw() -> T {
       // some implementation
   }
}

class PrizeBag extends DrawBag<Prize> {
   bagColor:string

   constructor(bagColor:string, numBalls:number, numGum:number) {
      let prizes: Prize[] = []
      // populate prizes array
      super(prizes)
      this.bagColor = bagColor
   }
}

So, I want to be able to serialize and revive PrizeBag, but I can't appropriately discriminate the items type. What I was wondering is if you could add an operator for discrimination ... perhaps '|' or something so you could do...

@Reviver<PrizeBag>({ 
    '.': Jsonizer.Self.assign(PrizeBag),
    items: {
        '|' : [ 'type', {  // <-- discriminator property followed by mapping for each value
                  'ball':Ball,
                  'gum':Gum
              } ]
    }
})
class PrizeBag extends DrawBag<Prize> {
   bagColor:string

   constructor(bagColor:string, numBalls:number, numGum:number) {
      let prizes: Prize[] = []
      // populate prizes array
      super(prizes)
      this.bagColor = bagColor
   }
}

The hope is that with such a definition, during revival it could look at the property and load the appropriate mapper.

Alternatively if you could help me understand a way to accomplish the same thing without any library changes I would welcome it!

ppoulard commented 6 months ago

Hi,

It seems that you need a kind of 'placeholder reviver' for the items, that is to say a class that serves to dispatch the items to their relevant concrete class ; it's just a class with a reviver :

@Reviver<Ball | Gum>({
    '.': (type, ...args) => {
            if (type === 'ball') {
                return new Ball(...args);
            } else if (type === 'gum') {
                return new Gum(...args);
            } else {
                throw new TypeError(`Oooops: that type ${type} is unkown...`);
            }
        }
})
class abstract PrizeItem {} // let's make it abstract to prevent having instances

// then your bag :
@Reviver<PrizeBag>({ 
    '.': Jsonizer.Self.assign(PrizeBag),
    items: {
         '*': PrizeItem
    }
}
class PrizeBag ...

Not tested, maybe some casting or any type is expected here and there, but the point is to delegate the job to that dispatcher reviver.

Hope this helps !