dhardiman/Config
Extensible configuration generation for projects in swift
Schemas
### Default
A sample of the schema is:
```
{
"template": {
"imports": [ "MyCustomFramework" ]
},
"key": {
"description": "An optional comment to document the property. Will be added as a comment to the generated code",
"type": "String",
"defaultValue": "value to be used by all schemes",
"overrides": {
"scheme pattern 1": "a different string to be used by schemes matching 'scheme pattern 1'",
"scheme pattern 2": "a different string to be used by schemes matching 'scheme pattern 2'"
}
},
"group: {
"key": {
"type": "String",
"defaultValue": "value to be used by all schemes",
"overrides": {
"scheme pattern 1": "a different string to be used by schemes matching 'scheme pattern 1'",
"scheme pattern 2": "a different string to be used by schemes matching 'scheme pattern 2'"
}
}
}
```
The "key" will be used as a static property name in a `class` so should have a format that is acceptable to the swift compiler. Most likely `lowerCamelCase`.
`type` can have the following values:
- `String`: A swift string value
- `URL`: A url. Will be converted to `URL(string: "the value")!`
- `Int`: An integer value.
- `Double`: A double value
- `Bool`: A boolean value
- `Colour`: A colour in hex format, will be output as a `UIColor`.
- `DynamicColour`: A pair of colours, in hex format, that will be output as an iOS 13-compatible dynamic colour.
- `DynamicColourReference`: A pair of colours, as properties on the current configuration or somewhere else, that will be output as an iOS 13-compatible dynamic colour.
- `Image`: The name of an image. Will be converted to `UIImage(named: "the value")!`.
- `Regex`: A regular expression pattern. Will be converted to `try! NSRegularExpression(patthen: "the value", options: [])`
- `EncryptionKey`: A key to use to encrypt sensitive info.
- `Encrypted`: A value that should be encrypted using the provided key
- `EnvironmentVariable`: A named environment variable, the value of which will be obtained from the current shell environment at runtime.
- `EncryptedEnvironmentVariable`: Encrypted version of `EnvironmentVariable` (requires `EncryptionKey` be defined).
- `Dictionary`: A dictionary. Keys should be strings, values in the dictionary should be either string, numeric, or a new dictionary.
- `Reference`: See [Reference Properties](#reference-properties) below.
- Enum types. Set the `type` to the name of the enum, set the value to be the case, preceded by a `.`, so `.thing`. If you need enums from a custom module, add a string array of imports to the template section.
`overrides` contains values that are different to the provided `defaultValue`. The keys in this dictionary should be a regex pattern to match the scheme passed in. The values should be the same type as the `defaultValue` as specified by `type`. If two overridden values could match, the first suitable value found is used. `overrides` is optional, if not provided, all schemes will use the `defaultValue`.
Note properties can also be grouped together as per the second example. Any number of properties can be added to a named group, which will create a nested class within the parent config class with the properties attached.
#### Associated Properties
Sometimes you may want to map a property to the output of another property, rather than a passed in scheme. Take the example below:
```
{
"host": {
"type": "String",
"defaultValue": "example.com",
"overrides": {
"test": "test.example.com",
"stage": "test.example.com",
"live": "live.example.com"
}
},
"logoName": {
"type": "String",
"defaultValue" "logo.png",
"associatedProperty": "host",
"overrides": {
"test.example.com": "logo-test.png"
}
}
}
```
The `logoName` property has an `associatedProperty`, which ties it's `overrides` to the value of `host` instead of the passed in scheme. This allows for more concise override lists, as in the example above both the "test" and "stage" scheme will produce a "logo-test.png" logoName.
Note that there are a couple of caveats when using `associatedProperty`:
- The keys in `overrides` do not use regular expression pattern matching, and instead require an exact string match.
- The `associatedProperty` _must_ have a String type.
#### Reference Properties
Sometimes you may want to make a property return the output of another property, depending on the passed in scheme. For example:
```
{
"red": {
"type": "Colour",
"defaultValue": "#FF0000"
},
"green": {
"type": "Colour",
"defaultValue": "#00FF00"
},
"textColour": {
"type": "Reference",
"defaultValue": "red",
"overrides": {
"greenScheme": "green"
}
}
}
```
The `textColour` property will be `return red` for all schemes bar the `greenScheme` where it will be `return green`.
### Enum
This schema should be used for creating enums.
A sample of the schema is:
```
{
"template": {
"name": "enum",
"rawType": "String"
},
"key": {
"defaultValue": "",
"overrides": {
"scheme pattern 1": "a dffierent string to be used by schemes matching 'scheme pattern 1'"
}
}
}
```
`template.name` defines which template code `config` should use to parse this file. `template.rawType` specifies the raw enum type to use. Currently only "String" is supported. The properties follow the same rules as the default, however `type` is not required. If no value is provided for `defaultValue` and no `overrides` are present, the enum key will also be the raw value.
### Extensions
This schema should be used for creating extensions on existing classes.
A Sample of the schema is:
```
{
"template": {
"extensionOn": "UIColor",
"extensionName": "Palette",
"requiresNonObjC": true
},
"brand": {
"type": "Colour",
"defaultValue": "#FF0000",
"overrides": {
"blue": "#0000FF"
}
}
}
```
This will output an extension on `UIColor` in a file called `UIColor+Palette.swift`.
### Custom types
It is possible to use your own custom types with config. Add a `customTypes` array to your `template` section and you can then add your values, either as a string, for single values, or as a keyed dictionary. For example:
```
{
"template": {
"customTypes": [
{
"typeName": "MyCustomType",
"initialiser": "MyCustomType(thing: {$0})"
},
{
"typeName": "MyMoreComplexCustomType",
"initialiser": "MyMoreComplexCustomType(thing: {thing}, otherThing: {otherThing:String})"
}
]
},
"myThing": {
"type": "MyCustomType",
"defaultValue": "Thingy"
},
"myOtherThing": {
"type": "MyMoreComplexCustomType",
"defaultValue": {
"thing": "Thingy",
"otherThing": "A different thingy"
}
}
}
```
Placeholders in the initialiser template should be written as `{key}` or `{key:TypeHint}` where the type hint is one of the basic primitive types, `Bool`, `String`, `URL`, `Int`, `Double`. If no type hint is supplied then the value is treated as an expression.
### Common patterns
If you find yourself repeating override patterns, for example `(PROD|STAGING)` you can list those in the `patterns` section of the template for reuse in your configuration. For example:
```
{
"template": {
"patterns": [
{
"alias": "prodAndStaging",
"pattern": "(PROD|STAGING)"
}
]
},
"myString": {
"type": "String",
"defaultValue": "Hello",
"overrides": {
"prodAndStaging": "Overridden value"
}
}
}
```
### DynamicColour and DynamicColourReference
To support iOS 13's dark mode, it is possible to output colours as dynamic. For example:
```
"background": {
"type": "DynamicColour",
"defaultValue": {
"light": "#FF",
"dark": "#00"
}
}
```
will output:
```
@nonobjc static var background: UIColor {
if #available(iOS 13, *) {
return UIColor(dynamicProvider: {
if $0.userInterfaceStyle == .dark {
return UIColor(white: 0.0 / 255.0, alpha: 1.0)
} else {
return UIColor(white: 255.0 / 255.0, alpha: 1.0)
}
})
} else {
return UIColor(white: 255.0 / 255.0, alpha: 1.0)
}
}
```
Similarly, it is possible to use references to another colour, so:
```
"background": {
"type": "DynamicColourReference",
"defaultValue": {
"light": "UIColor.white",
"dark": "UIColor.black"
}
}
```
will output:
```
@nonobjc static var background: UIColor {
if #available(iOS 13, *) {
return UIColor(dynamicProvider: {
if $0.userInterfaceStyle == .dark {
return UIColor.black
} else {
return UIColor.white
}
})
} else {
return UIColor.white
}
}
```Writing your own schemas
Just add a new class or struct to the project and implement Template. Add your new parser to the templates array in main.swift. Your template should inspect a template dictionary in any config and decide whether it can parse it. Either using a name item, or through other means. Ensure ConfigurationFile is the last item in that array. As the default schema parser it claims to be able to parse all files.
As new templates can be written from scratch, there is no pre-defined schema that your json file should adhere to, but for the sake of readability for other contributors, it would probably be sensible if it resembled the default schema as closely as possible.
Running as a build phase
If running as a build phase within Xcode, it's recommended to use xcode file lists, both for the source input and the generated files. The input file list will ensure Xcode expires its cache of files and you build with the latest version. The output file list will ensure Xcode waits for your files to be generated before proceeding.
Package Metadata
Repository: dhardiman/Config
Stars: 2
Forks: 5
Open issues: 0
Default branch: master
Primary language: swift
License: MIT
README: README.md