Skip to main content

polycons

Project description

Polycons

A framework for building polymorphic constructs. Think of polycons like dependency injection for constructs.

polycons can be used with any CDK framework, including AWS CDK, cdktf, and cdk8s.

🚀 Getting started

Polycons can be used just like ordinary constructs:

import { Dog } from "@acme/shared-polycons";
import { Construct } from "constructs";

class Pets extends Construct {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    // this is a polycon!
    new Dog(this, "Dog", { treats: 5 });

    // like ordinary constructs, polycons can have methods, properties, etc.
    dog.giveBone();
  }
}

This Pets construct contains a Dog from a library of polycons. The dog could have multiple implementations -- a Labrador, a Terrier, or your own implementation.

To use polycons in an application, you need to register a factory that specifies how to turn polycons into concrete constructs. In the example below, a PetFactory is used, which has been defined to resolve each Dog in the construct tree into a Labrador. By registering it to the root App construct, each Dog in the construct tree will be created as a Labrador.

import { App } from "<cdk-framework>";
import { PetFactory } from "@acme/shared-polycons";
import { Polycons } from "polycons";

const app = new App();
Polycons.register(app, new PetFactory());
new Pets(app, "MyPets");

Check out the usage guide for more details about how to create your own polycons and polycon factories.

📖 Documentation

Click here to visit the polycons API reference.

🏭 Polycon factories

A polycon factory is a class that implements the IPolyconFactory interface, which has a single resolve() method. This method accepts a type and a list of construct arguments, and returns a concrete construct. For example:

import { DOG_TYPE, CAT_TYPE, Labrador, Kitten } from "@acme/shared-polycons";

class PetFactory implements IPolyconFactory {
  public resolve(
    type: string,
    scope: Construct,
    id: string,
    ...args: any[]
  ): Construct {
    switch (type) {
      case DOG_TYPE:
        return new Labrador(scope, id, ...args);
      case CAT_TYPE:
        return new Kitten(scope, id, ...args);
      default:
        throw new Error(`Type "${type}" not implemented.`);
    }
  }
}

In the above example, DOG_TYPE and CAT_TYPE are unique string constants associated with the respective polycons.

By customizing the resolve() method, it's possible to change construct IDs, override properties, or even make factories that call other factories.

✍️ Creating polycons

You can define a new polycon by creating a class that returns a new Polycon instance in its constructor. Each polycon must be associated with a unique identifying string.

import { Constructs } from "constructs";
import { Polycons } from "polycons";

export interface DogProps {
  readonly name?: string;
  readonly treats?: number;
}

// make sure your polycon has a globally unique name!
export const DOG_TYPE = "@acme/shared-polycons.Dog";

export class Dog extends Construct {
  constructor(scope: Construct, id: string, props: DogProps) {
    super(null as any, id); // (1)
    return Polycons.newInstance(DOG_TYPE, scope, id, props) as Dog;
  }
}

The Dog class definition serves as an empty shell, or placeholder -- only when a user calls new Dog(), a real construct will be returned.

In the constructor of Dog, a null value MUST be passed as the first argument to super() (1). This is because actually two constructs are made by the constructor, and the first one should be thrown away (and not be added to the construct tree).

Concrete implementations of a polycon can be written like ordinary constructs:

export class Labrador extends Construct {
  public readonly name: string;
  private readonly treats: number;
  constructor(scope: Construct, id: string, props: DogProps) {
    super(scope, id);
    this.name = props.name;
    this.treats = props.treats;
  }
  public toString() {
    return `Labrador with ${this.treats} treats.`;
  }
}

🤝 Sharing behavior

Oftentimes, you may want all polycons to share some properties or methods.

You can achieve this by defining a base class, and having the polycon extend the base class:

export interface DogProps {
  readonly name?: string;
  readonly treats?: number;
}

export const DOG_TYPE = "@acme/shared-polycons.Dog";

// This is the polycon.
export class Dog extends DogBase {
  constructor(scope: Construct, id: string, props: DogProps) {
    super(null as any, id, props); // [1]
    return Polycons.newInstance(DOG_TYPE, scope, id, props) as Dog;
  }
  public toString() {
    throw new Error("Method not implemented"); // [2]
  }
}

// This is the base class.
export abstract class DogBase extends Construct {
  public readonly species = "Canis familiaris";
  public readonly treats: number;
  constructor(scope: Construct, id: string, props: DogProps) {
    super(scope, id);

    // [3]
    if (!scope) {
      this.treats = 0;
      return;
    }

    this.treats = props.treats;
  }
  public abstract toString(): string;
}

Please take note:

  1. In the constructor of the polycon (Dog), a null value MUST be passed as the first argument to super().
  2. Since the Dog class is just an empty shell, and does not get returned to the user, any methods required by the abstract base class can be left unimplemented.
  3. In the constructor of the base class (DogBase), the constructor should have no side effects or mutations when an empty scope is passed (otherwise, side effects may occur multiple times). In the example above, we set dummy values when the scope is empty (this.treats = 0;) and return early.

When a polycon has a base class, every polycon implementation should extend it instead of extending Construct:

export class Labrador extends DogBase {
  public readonly name: string;
  constructor(scope: Construct, id: string, props: DogProps) {
    super(scope, id, props);
    this.name = props.name;
  }
  public toString() {
    return `Labrador with ${this.treats} treats.`;
  }
}

✋ Contributing

We welcome community contributions and pull requests. See CONTRIBUTING.md for information on how to set up a development environment and submit code on GitHub.

⚖️ License

This library is licensed under the Apache-2.0 license.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

polycons-0.1.5.tar.gz (24.5 kB view details)

Uploaded Source

Built Distribution

polycons-0.1.5-py3-none-any.whl (23.0 kB view details)

Uploaded Python 3

File details

Details for the file polycons-0.1.5.tar.gz.

File metadata

  • Download URL: polycons-0.1.5.tar.gz
  • Upload date:
  • Size: 24.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.2 CPython/3.11.0

File hashes

Hashes for polycons-0.1.5.tar.gz
Algorithm Hash digest
SHA256 fcfa31b1823bdb639493160dcce98d75c10bad8b69ada832261a32108c21194e
MD5 3fbbc43e0122235640bcbd47a923c8b5
BLAKE2b-256 e8409d48c95fc7e6071ddcc6ff936c0612ac7fbb0b0ccb088f500f4cce66d7cb

See more details on using hashes here.

File details

Details for the file polycons-0.1.5-py3-none-any.whl.

File metadata

  • Download URL: polycons-0.1.5-py3-none-any.whl
  • Upload date:
  • Size: 23.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.2 CPython/3.11.0

File hashes

Hashes for polycons-0.1.5-py3-none-any.whl
Algorithm Hash digest
SHA256 5786f568b3379e1363fccd27b6552f486d71d3f251e643b5da954944f29a5ea4
MD5 0f79b7d8a5d811c6bd6dcc560b6944d7
BLAKE2b-256 0877714a26bf86e835fc0a44c7ce412d7fba61a0837b8de83f0d3945c3370de5

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page