Skip to content

Tiny but powerful library creating your java objects with a bit of magical pixie dust

License

Notifications You must be signed in to change notification settings

tomitribe/pixie

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

33 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Pixie

Pixie is a tiny 100k jar that handles configuration, dependency injection and events. It can handle any scenario where you would use reflection to instantiate a Java object.

Maven dependency:
<dependency>
  <groupId>org.tomitribe.pixie</groupId>
  <artifactId>pixie</artifactId>
  <version>2.0</version>
</dependency>

Example

Let’s imagine we want to use Pixie to configure and build two classes we’ve created called Person and Address.

Our first step is to annotate the constructor parameters with Pixie annotations. Pixie will map the configuration to our constructor based on the names we use.

import org.tomitribe.pixie.Component;
import org.tomitribe.pixie.Default;
import org.tomitribe.pixie.Name;
import org.tomitribe.pixie.Param;

public class Person {

    private final String name;
    private final Integer age;
    private final Address address;

    public Person(@Name final String name,
                  @Param("age") final Integer age,
                  @Component("address") final Address address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }

    //...
}

public class Address {
    private final String street;
    private final String city;
    private final State state;
    private final int zipcode;
    private final String country;

    public Address(@Param("street") final String street,
                   @Param("city") final String city,
                   @Param("state") final State state,
                   @Param("zipcode") final int zipcode,
                   @Param("country") @Default("USA") final String country) {
        this.street = street;
        this.city = city;
        this.state = state;
        this.zipcode = zipcode;
        this.country = country;
    }
    //...
}

public enum State {
    WI, MN, CA;
}

We can use the Pixie System to configure and build the above objects via either a Properties file defintion or in code via a Builder API

Properties

Our properties file might look like so. The file name and location can be anything as it’s our job to read this into a java.util.Properties instance.

jon = new://org.example.Person
jon.age = 46
jon.address = @home

home = new://org.example.Address
home.street = 823 Roosevelt Street
home.city = River Falls
home.state = WI
home.zipcode = 54022
home.country = USA

The following code would build a Pixie System via a java.util.Properties instance and lookup the Person instance by type.

import java.util.Properties;
import org.example.Person;
import org.tomitribe.pixie.System;

//...

public static void main(String[]) {

    final Properties properties = new Properties();
    properties.load(...); // read the properties file

    final System system = new System(properties);

    final Person person = system.get(Person.class);
    assertEquals("jane", person.getName();

    final Address address = person.getAddress();
    assertNotNull(address);
    assertEquals("820 Roosevelt Street", address.getStreet());

}

Builder API

Alternatively, we can skip the properties and build our System in code fluently.

import java.util.Properties;
import org.example.Person;
import org.tomitribe.pixie.System;

//...

public static void main(String[]) {

    final System system = System.builder()

            .definition(Person.class, "jane")
            .param("age", 37)
            .comp("address", "home")

            .definition(Address.class, "home")
            .param("street", "820 Roosevelt Street")
            .param("city", "River Falls")
            .param("state", "WI")
            .param("zipcode", "54022")

            .build();

    final Person person = system.get(Person.class);

    //...
}

Pixie Constructor Annotations

Pixie supports constructor injection. Each parameter the constructor Pixie will use must be annotated with either @Param, @Component, @Event or @Name.

Annotation Purpose Example Usage

@Param

Maps a constructor parameter to a config property

@Param("username") final String username

@Default

Provides a default value if the property is missing

@Param("country") @Default("USA") final String country

@Component

Injects a dependent object built by or given to Pixie System

@Component final PaymentProcessor paymentProcessor

@Nullable

Allows a property to be null if missing

@Nullable @Param("footer") final String footer

@Name

Injects the component’s name from the config

@Name final String serviceName

@Event

Injects a Consumer<T> to fire events

@Event final Consumer<OrderPlaced> event

@Observes

Marks a method as an event listener

public void onEvent(@Observes OrderPlaced event)

All the above annotations are in the org.tomitribe.pixie package.

@Param

Purpose: Binds a constructor parameter to a configuration property.

Usage: Pixie will automatically inject values from a properties file or the builder API.

Example:
public final class User {
    private final String username;
    private final int age;

    public User(@Param("username") final String username, @Param("age") final int age) {
        this.username = username;
        this.age = age;
    }
}

Maps to a properties file entry:

user=new://org.example.User
user.username=alice
user.age=30

Any Java type that can be created from a String is supported. Pixie will inspect the java class and look for one of the following:

  • Public constructor with a single parameter of type String

  • Public static method with a single parameter of type String returning an instance of the type


@Default

Purpose: Specifies a default value for a constructor parameter if it is not set in the configuration.

Example:
public final class Address {
    private final String country;

    public Address(@Param("country") @Default("USA") final String country) {
        this.country = country;
    }
}

If country is missing from the config, "USA" is used. Applies to both @Param and @Component. When used on @Component it implies the name of the component that should be injected.


@Component

Purpose: Indicates that a constructor parameter should be injected as a component dependency.

Example:
public final class ShoppingCart {
    private final PaymentProcessor paymentProcessor;

    public ShoppingCart(@Component("processor") final PaymentProcessor paymentProcessor) {
        this.paymentProcessor = paymentProcessor;
    }
}

Pixie can resolve this reference by name or by type.

Resolution by name:
cart=new://org.example.ShoppingCart
cart.processor=@stripe

With the above configuration Pixie will look in the System for an object with the name stripe and inject it as the value of the processor when constructing the ShoppingCart.

A ConstructionFailedException will be thrown if no object with that name is found or if the object found is of the wrong type.

Resolution by type:
cart=new://org.example.ShoppingCart

In the above configuration the processor name has not be specified. In this situation, Pixie will look in the System for any object with the the type PaymentProcessor and inject it as the value of the processor when constructing the ShoppingCart.

If there are multiple instances of PaymentProcessor they will be sorted in descending order by name and the first will be picked.

A ConstructionFailedException will be thrown if no objects with that type are found.

Adding Custom Components

The @Component annotation can be used to resolve components which are added directly to the Pixie System.

Properties:
jane=new://org.example.Person\n" +
jane.age = 37
jane.address=@home

In the above properties, the Person object has a @Component reference to Address called home which is not defined. The home instance can be added directly to the Pixie System before we load the properties.

Adding to Pixie System:
final Properties properties = //...

final System system = new System();
system.add("home", new Address("820 Roosevelt Street","River Falls", State.WI, 54022, "USA"));
system.load(properties);

final Person person = system.get(Person.class);
assertNotNull(person.getAddress());

In the above code we’ve directly created the Address instance and handed it to Pixie System with the name home.


@Nullable

Purpose: Marks a constructor parameter as optional (can be null if not configured).

Example:
public final class Notification {
    private final String message;
    private final String footer;

    public Notification(@Param("message") final String message, @Nullable @Param("footer") final String footer) {
        this.message = message;
        this.footer = footer;
    }
}

If footer is missing from the config, it will be null instead of throwing an error.


@Name

Purpose: Injects the component’s name from the configuration.

Example:
public final class Service {
    private final String serviceName;

    public Service(@Name final String serviceName) {
        this.serviceName = serviceName;
    }
}

If configured as myService = new://com.foo.Service, the constructor will receive "myService".


@Event

Purpose: Injects an event consumer (Consumer<T>) into a component so it can fire events.

Example:
public final class OrderService {
    private final Consumer<OrderPlaced> orderPlacedEvent;

    public OrderService(@Event final Consumer<OrderPlaced> orderPlacedEvent) {
        this.orderPlacedEvent = orderPlacedEvent;
    }

    public void placeOrder(final String orderId) {
        orderPlacedEvent.accept(new OrderPlaced(orderId));
    }
}

Pixie will inject a Consumer<OrderPlaced> that calls System.fire(event) which will invoke all observer methods in all components in the System.


@Observes

Purpose: Marks a method as an event listener.

Example:
public final class OrderListener {
    public void onOrderPlaced(@Observes final OrderPlaced event) {
        System.out.println("Order placed: " + event.getOrderId());
    }
}

When OrderPlaced is fired, this method will be called automatically.

It is possible to listen for events by any assignable type, even java.lang.Object

Example:
public final class EverythingListener {
    public void onEvent(@Observes final Object event) {
        System.out.println("Event observed: " + event);
    }
}

Configuration Validation

Pixie provides strict validation to ensure configuration correctness and prevent common issues with properties files.

Case Insensitivity

All properties in Pixie are case insensitive, meaning users will not encounter failures due to incorrect capitalization. For example, the following entries are treated as equivalent:

user.name=Alice
User.Name=Alice
USER.NAME=Alice

Regardless of how the property is written, it will be correctly matched and retrieved.

Strict Property Validation

Pixie enforces strict validation of configuration properties to prevent misconfigurations:

  • If a property is specified in the configuration file but does not exist in the corresponding class, Pixie will throw an exception at startup.

  • This ensures that typos or removed properties do not lead to silent failures.

For example, given the following properties file:

app.mode=production
app.timeout=5000

If the app.timeout property is removed from the Java class but remains in the configuration file, Pixie will fail fast with an error, preventing users from relying on "dead" properties.

About

Tiny but powerful library creating your java objects with a bit of magical pixie dust

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages