Mixin tagged variants with unboxed

I have a JSON schema that will produce data like (per line):

{"block_type": "page-break-1.0", "break_type": "soft"}
{"block_type": "custom-section-1.0", "name": "Some section", "pk": "uuid"}
{"block_type": "custom-section-template-1.0", "name": "Some section"}

This was my first attempt this:

module LevelId = {
  type t = string
}

module Block = {
  type t = {uuid: string}
}

module PageBreak = {
  type t = {...Block.t, break_type: [#soft | #hard]}
}

module CustomSection = {
  type t = {...Block.t, name: string, level_id: LevelId.t}
}

module CustomSectionTemplate = {
  type t = {...Block.t, name: string}
}

module ReportBlock = {
  @tag("block_type")
  type t =
    | @as("page-break-1.0") PageBreak(PageBreak.t)
    | @as("custom-section-1.0") CustomSection(CustomSection.t)
}

module ReportTemplateBlock = {
  @tag("block_type")
  type t =
    | @as("page-break-1.0") PageBreak(PageBreak.t)
    | @as("custom-section-template-1.0")
    CustomSectionTemplate(CustomSectionTemplate.t)
}

module Examples = {
  let x: ReportBlock.t = PageBreak({uuid: "1", break_type: #soft})
  let y: ReportBlock.t = CustomSection({
    uuid: "2",
    name: "Some section",
    level_id: "0000-0000",
  })

  let x1: ReportTemplateBlock.t = PageBreak({uuid: "3", break_type: #soft})
  let y1: ReportTemplateBlock.t = CustomSectionTemplate({
    uuid: "4",
    name: "Some section",
  })
}

However that would not match the JSON data.

I tried to (mis)use the @unboxed and breaks the tag:

module ReportBlock = {
  @tag("block_type")
  type t =
    | @as("page-break-1.0") @unboxed PageBreak(PageBreak.t)
    | @as("custom-section-1.0") @unboxed CustomSection(CustomSection.t)
}

That produces JS records without the block_type:

var Examples_x = {
  uuid: "1",
  break_type: "soft"
};

var Examples_y = {
  uuid: "2",
  name: "Some section",
  level_id: "0000-0000"
};

The only “solution” I have now is to inline the types like here:

module ReportBlock = {
  @tag("block_type")
  type t =
    | @as("page-break-1.0") PageBreak({uuid: string, break_type: [#soft | #hard]})
    | @as("custom-section-1.0") CustomSection({uuid: string, name: string, level_id: string})
}

But that would make the types non-shared between the two main types ReportBlock.t, and ReportTemplateBlock.t.

Is there a way to overcome this?

The trick I’ve found is to define the component parts as single-element variants, then use variant spreads. It does mean that the shared Block module doesn’t really work, but it’s at least an option for a tradeoff.

module LevelId = {
  type t = string
}

module PageBreak = {
  @tag("block_type")
  type t = 
    | @as("page-break-1.0") PageBreak({uuid: string, break_type: [#soft | #hard]})
}

module CustomSection = {
  @tag("block_type")
  type t = 
    | @as("custom-section-1.0") CustomSection({uuid: string, name: string, level_id: LevelId.t})
}

module CustomSectionTemplate = {
  @tag("block_type")
  type t = 
    | @as("custom-section-template-1.0") CustomSectionTemplate({uuid: string, name: string})
}

module ReportBlock = {
  @tag("block_type")
  type t =
    | ...PageBreak.t
    | ...CustomSection.t
}

module ReportTemplateBlock = {
  @tag("block_type")
  type t =
    | ...PageBreak.t
    | ...CustomSectionTemplate.t
}

module Examples = {
  let x: ReportBlock.t = PageBreak({uuid: "1", break_type: #soft})
  let y: ReportBlock.t = CustomSection({
    uuid: "2",
    name: "Some section",
    level_id: "0000-0000",
  })

  let x1: ReportTemplateBlock.t = PageBreak({uuid: "3", break_type: #soft})
  let y1: ReportTemplateBlock.t = CustomSectionTemplate({
    uuid: "4",
    name: "Some section",
  })
}

I used this pretty liberally in my graphql bindings for codegen.

3 Likes