Component definition files
Reference | Component Container
Component definition files are JSON files in which you declare which Go structs you want Granitic to manage as components, the relationships between your components and how configuration should be found for your components. They are loaded by the grnc-bind command line tool and transformed into Go source code. This process is known as binding.
File types, names and locations
Your component definition files must be valid UTF-8 encoded JSON files with a .json
extension. You may split your
components across as many files as you see fit.
You can store these files in any location that is accessible to the grnc-bind
tool but component definition files
are generally considered part of of your application’s build-time assets so would normally be checked into version control.
The default location for component definition files is in a folder called comp-def
under your application’s project folder.
Merging
The grnc-bind command line tool loads all of your component definition files into memory and merges them
together. It is highly recommended that a component’s declaration is contained within a single file. If you want to see
the effective merged component definition file that grnc-bind
creates you can run grnc-bind with the -m flag set
Packages
Like Go source files, your component definition files need to state which Go packages contain the types you want to
use for your components. Packages are specified in a string array named packages
at the root of your component definition
file’s JSON structure.
{
"packages": [
"github.com/graniticio/granitic/v2/ws/handler",
"github.com/graniticio/granitic/v2/validate",
"recordstore/artist",
"recordstore/db",
"github.com/go-sql-driver/mysql"
]
}
If you have multiple component definition files you may declare packages in each of them. The list of packages to
import is merged together by grnc-bind
and any duplicates are discarded.
Aliases
If you import two or more packages where the final part of the package names clash (e.g "myapp/handler"
and
github.com/graniticio/granitic/v2/ws/handler"
) you can assign an alias to one of the clashing packages in the JSON
object/map packageAliases
at the root of your component definition file’s JSON structure:
{
"packageAliases": {
"mh": "myapp/handler"
}
}
Note that a package should be defined in either the packageAliases
section or the packages
section, not both.
Components
You define your application’s components in the components
JSON object/map at the root of your component definition file’s JSON structure:
{
"components": {
"myHandler": {
"type": "my.Type"
}
}
}
The minimum requirements for a valid component are that it has a unique name (myHandler
in the example above) and either
a type (my.Type
in the examples above) or a reference to a component template from which the type can be inferred (see below).
Names
A component name must be unique within your application. Names must also follow the rules for Go variable name with one exception - there is no significance attached to whether the first letter of the name is upper or lower case. Granitic has a weak convention that component names start with a lower case letter, by you are free to ignore that convention.
Types
Component types are formatted as the same way you would use an imported struct in a Go source file, with the last part of the containing package’s name following by the name of the type itself.
Component configuration
After Granitic instantiates your component, it can inject values into any exported field on the underlying struct. Values can either be:
- Defined explicitly in a component’s declaration
- Deferred with a ‘configuration promise’
Explicit values
Values that will not change according to the environment in which your application will run, and are not sensitive in any way, can be directly specified as part of the component’s declaration.
For example:
"submitArtistHandler": {
"type": "handler.WsHandler",
"HTTPMethod": "POST",
"PathPattern": "^/artist[/]?$"
}
declares a web service handler using Granitic’s built-in WsHandler
type. This is an example of
‘static configuration’ where your component’s behaviour is configurable but will not change between environments
(you will always want this handler to be an HTTP POST handler and match the same path pattern).
Security
It is important to note that configuration defined in component definition files will be compiled into your application so it is strongly recommended that your do not specify passwords or other sensitive information in these files.
Configuration promises
Configuration that will change between environments, is sensitive in some way or may need to be changed without rebuilding your application should be defined in files or URLs that are made available to your application when it starts. For each field that should be configured in this way you provide a configuration promise
For example:
"dbConfig": {
"type": "mysql.Config",
"User": "$database.user",
"Passwd": "$database.password",
"Addr": "$database.host",
"DBName": "$database.instance",
"AllowNativePasswords": true
}
declares a component that will store credentials and other configuration for accessing a database. Each field that
could change between environments is associated with a configuration promise. In this case we are promising that
a JSON configuration file will be available at application start time that has an object called database
in
the root of the file with a series of fields containing actual configuration values. The route to a particular value (e.g
database.instance
is called a configuration path)
Granitic will automatically inject the values found into your component.
The $ prefix
The $
symbol in front of each configuration path tells grnc-bind
that this string is a configuration promise and not
an explict string value that you want injected into your component. If you do have a string value starting with a $
,
you can escape it with $$
If you find the $
notation difficult to read, you can use conf:
or c:
as an alternative.
Default values
Occasionally it is convenient to specify a value to be used if the configuration path cannot be found at application startup time. In these circumstances you can supply a default value in round brackets after the configuration promise.
For example:
"dbConfig": {
"Addr": "$database.host(localhost)"
}
This is not generally the recommend approach - instead you should maintain a base configuration file that contains the default values for any configuration paths your application relies upon.
References to other components
Granitic can link your components together by automatically injecting references. The only requirement is that the receiving component must have a field that is typed as a pointer to the type that is being injected.
For example:
type A struct {}
type B struct {
ComponentA *A
}
Explicit references
The most common way of linking two components is to explicitly declare the relationship in your component definition file.
For example:
{
"components": {
"dbConfig": {
"type": "mysql.Config"
},
"dbProvider": {
"type": "db.MySQLProvider",
"Config": "+dbConfig"
}
}
}
The + prefix
The +
symbol tells Granitic that the contents of that string is the name of another component to be injected into
the field. In the above example, the instance of mysql.Config
associated with the dbConfig
component will be injected
into the field Config *mysql.Config
on the dbProvider
component.
If you find the +
symbol too subtle, you can use ref:
or r:
instead.
Nested components
If you have a component that will only realistically be referenced by one other component, it is possible to nest your component declarations to make this clear.
For example:
"submitArtistHandler": {
"type": "handler.WsHandler",
"HTTPMethod": "POST",
"PathPattern": "^/artist[/]?$",
"Logic": {
"type": "artist.PostLogic"
}
}
In this example the component that is to be injected into the submitArtistHandler
’s Logic
field is being declared
inline.
Names allocated to nested components
Nested components are effectively anonymous so cannot be referred to by other components or manipulated through runtime control. You can explicitly assign a name to a nested component to get around this limitation.
For example:
"submitArtistHandler": {
"type": "handler.WsHandler",
"Logic": {
"type": "artist.PostLogic"
"name": "submitLogic"
}
}
The nested component in the above example now has the name submitLogic
.
Decorators
There are occasionally circumstances where references cannot or should not be explicitly defined in component definition files. Granitic supports a pattern of decoration where references between components can be made programmatically. This decorator pattern is described here
Framework modifiers
In order to meet the principle of easily replaced core features, Granitic provides a mechanism for modifying its internal components via configuration. This feature is known as framework modification and is considered an advanced feature and carries some risk as it makes your code dependent on internal Granitic naming and code structure that might be refactored in a later release.
Some sections of in this reference manual may suggest you use framework modification in lieu of more robust customisation that is expected in a later release of Granitic.
Modifying a component
Granitic manages its internal components in the same container as the user components that you create. By convention, these
components are named with the prefix grnc
. In order to modify a framework component you must know its name. The facility
documentation generally lists the name and type of the components created by each facility.
Once you have the name of the component you want to modify, you need to add a new section to your component definition file
(for this example we are modifying the grncJSONResponseWriter
component that is part of the JSONWs facility)
"frameworkModifiers": {
"grncJSONResponseWriter": {
"StatusDeterminer": "myCustomHttpCodes"
}
}
Here we are telling Granitic to override the component that is injected into the StatusDeterminer
field of grncJSONResponseWriter
(which is an instance of ws.MarshallingResponseWriter)
with a different component. The component you inject can be any component that you have defined in the components
section
of your component definition file.
Note that the name of the injected component is not prefixed with a +
, which would normally indicate a reference to another
component. That is because framework modification only supports the injection of components, not literal values or
configuration promises.
Next: Component templates