Prickle is a lightweight configuration format based on JSON. Prickle aims to be simple, accessible to everyday users, and be very easy to parse and generate with code.
Config properties are wrapped in an object. This allows metadata and other
properties to be associated with the value. The value itself is held by the
value
key on this object. The format of the value is up to the schema of
the config, it can be a JSON primitive, JSON object, or even another config
property.
Standard JSON
{
"database_host": "192.168.1.0"
}
Prickle Config Property
{
"database_host": {
"value": "192.168.1.0"
}
}
The //
key is used to define a comment on a config property. Comments should
only be used to convey meaning to the user and should have no influence on how
value is parsed or used in code. The value can be a JSON string or an array of
JSON strings for multiline comments.
{
"database_host": {
"//": "The IP address of the database to connect to.",
"value": "192.168.1.0"
}
}
{
"database_host": {
"//": [
"The IP address of the database to connect to.",
"The port can be suffixed using a colon. "
],
"value": "192.168.1.0"
}
}
Decorators are a type of comment that exist to convey a specific attribute of
the property to the reader. Decorators are declared using the comment key
followed by an attribute name. For example the //default
decorator might be
used to denote the default value.
Decorators and their meanings are entirely up to the author of the config however the following decorators were pretty common during the testing and development of Prickle.
//default
- The default value for the property.//reference
- Points the reader to another resource that can be used to learn more about the property, like a wiki/docs page.//range
- A number property that must be within a specific range.//regex
- A string that must match the regex pattern.//empty-allowed
- A collection/array that is/isn't allowed to be empty.
Examples
{
"database_host": {
"//": [
"The IP address of the database to connect to.",
"The port can be suffixed using a colon. "
],
"//default": "localhost:4321",
"value": "192.168.1.0"
}
}
{
"ranged_int": {
"//": "A number that must fit within a given range.",
"//range": ">=0 AND <=100",
"value": 84
}
}
Prickle is the collective noun for a group of hedgehogs. Hedgehogs are one of my favourite animals and I think they are an oddly fitting metaphor for config files. For example, both may seem intimidating at first but can be nice once you get to know and understand them.
Before working on Prickle I spent several months test-driving existing formats and their libraries. While there are several formats that I liked I was disappointed with a lot of the Java implementations. Every library that I tried had serious long-standing bugs, was not being maintained, and lacked features that were important to me. After weighing my options I realized JSON could easily meet all of my criteria and the tools/libraries to work with JSON are already widely available.
Yes, feel free to create and distribute any tools or libraries for Prickle. I have chosen to release Prickle as a Java library because that is what I need however the format can be easily adapted for other platforms.
Currently just this one. If any more libraries are created I will update this section to include them.
This project is currently hosted on the BlameJared maven.
repositories {
maven {
url 'https://maven.blamejared.com'
}
}
dependencies {
implementation group: 'net.darkhax.prickle', name: 'Prickle', version: '1.0.6'
}
This library uses annotated Java objects to generate a config schema. This process is similar to POJO serialization in projects like GSON.
The first step in creating a config is writing a class that defines its properties. Properties are mapped using the fields of this class and annotations on those fields are used to customize how those properties are managed. When the config is loaded these fields will automatically be updated with the values that were read from the file.
Only the fields marked with the Value
annotation will be mapped to config
properties. For example this Java class will generate the following config.
public class ExampleConfig {
@Value
public String database = "localhost:4321";
public boolean readOnly = true;
}
{
"database": {
"//default": "localhost:1234",
"value": "localhost:1234"
}
}
The Value
annotation has some extra properties that can be used to change how
this library handles the property.
name
- An alternative name used to serialize the property.comment
- A comment for the value. Comments longer than 80 characters will be wrapped into multi-line comments that are padded to that line length.reference
- A link to an online resource that the reader can use to learn more about the value.writeDefault
- The default value will be included by default, but you can disable it with this.
You may want to limit numbers to a certain range. This can be done using the
various ranged number annotations. We currently support RangedInt
,
RangedLong
, RangedFloat
, and RangedDouble
. The library will validate the
value is within the defined range every time the value is loaded.
public class ExampleConfig {
@Value(name="ranged_int", comment="A number that must fit within a given range.")
@RangedInt(min=0, max=100)
public int aRangedInt = 84;
}
{
"ranged_int": {
"//": "A number that must fit within a given range.",
"//range": ">=0 AND <=100",
"//default": 84,
"value": 84
}
}
String properties can be validated using a regex pattern. This pattern can be
specified using the Regex
annotation. The library will validate the value
matches this pattern every time the value is loaded.
public class ExampleConfig {
@Value(name="logo_file", comment="The logo file to display.")
@Regex("^.*\\.(jpg|png)$")
public String file = "logo.png";
}
{
"logo_file": {
"//": "The logo file to display.",
"//regex": "^.*\\.(jpg|png)$",
"//default": "logo.png",
"value": "resources/my_logo.png"
}
}
All arrays and collections will be converted to prickle array properties by
default. These are mostly identical to JSON arrays but have a few extra
features that can be configured using the Array
annotation.
Arrays with five or fewer entries that only contain JSON primitives will be
inlined. This means that the declaration of the array and its values will be
printed on the same line. The threshold can be changed from five using the
inlineCount
of the annotation. You can also enable inlining non-primitive
values by setting inlineComplex
to true.
You can also set allowEmpty
to false which will validate that the array is
not empty when the value is read. All array properties can be empty by default.
public class ExampleConfig {
@Value(comment = "An inlined array")
public int[] intArray = {1, 2, 3, 4};
@Value(comment = "Not inlined", writeDefault = false)
@Array(inlineCount = 3)
public char[] charArray = {'a', 'b', 'c', 'd'};
@Value(comment = "An array that must not be empty.")
public List<String> stringList = List.of("first", "second", "third");
}
{
"intArray": {
"//": "An inlined array",
"//default": [1,2,3,4],
"value": [1,2,3,4]
},
"charArray": {
"//": "Not inlined",
"value": [
"a",
"b",
"c",
"d"
]
},
"stringList": {
"//": "An array that must not be empty.",
"//default": ["first","second","third"],
"value": ["first","second","third"]
}
}
In the previous section you learned how to create a config object class. This class can be read and written using a ConfigManager. If the file does not exist it will be generated and saved when you try to load it automatically.
public static void main(String... args) {
final ExampleConfig config = new ExampleConfig();
final ConfigManager<ExampleConfig> manager = new ConfigManager.Builder<ExampleConfig>(Path.of("example.json")).build(config);
manager.load();
System.out.println(config.toString());
manager.save();
}
The builder has some helpful options that allow you to further customize your config file.
logger(Logger)
- Sets the logger used for errors and warnings. If the logger is not specified we will create one using the name of the config object class.adapter(IPropertyAdapter)
- Registers a new property adapter. These let you handle how fields are mapped to config properties. The property can control how the value is serialized.gsonConfig(Consumer<GsonBuilder>)
- Lets you configure the underlying GSON instance.gsonConfig(Function<GsonBuilder, GsonBuilder>)
- Lets you configure the underlying GSON instance, or even replace it.