Getting Started
Lets say we have a Post
struct that contains the boolean field :active
. As time goes by, we recognize that there may be more kinds of statuses that our Post
's may have.
To keep up with the times, we replace our :active
field with the enum field :status
.
One of the values could be "active"
- among many others.
We arent ready to release this feature into the the wild yet though. So, while we change the internal structure of our Post
data, we must ensure we dont break the API contract we made with our users. Versioning to the rescue...
Versioning Struct
A the heart of our versioning is the Versioning
struct. A Versioning
struct contains the following fields:
:current
- The current version that our data represents.:target
- The version that we want our data to be changed into.:type
- The type of data we are working with. If we are working with structs, this will typically be the struct name, eg:Post
:data
- The underlying data that we want to change. For structs, like ourPost
, be aware that we typically have our data as a bare map since it is easier to transform.:changed
- A boolean representing whether a change operation has occured.:assigns
- A map of arbitrary data we can use to store additonal information in.
Let's see a couple different ways we can create a versioning a Post
.
# The type is automatically inferred from the struct module.
Versioning.new(%Post{}, "2.0.0", "1.0.0")
# We can also explicely set the type.
Versioning.new(%{}, "2.0.0", "1.0.0", Post)
# We can also build up our versioning using helpers.
|> Versioning.new()
|> Versioning.put_data(post)
|> Versioning.put_type(Post)
|> Versioning.put_current("2.0.0")
|> Versioning.put_target("1.0.0")
We now have a versioning of our Post
.
Versioning Change
Lets create of first "versioning change". This is a module that adheres to the
Versioning.Change
behaviour. From it, we must implement the callbacks up/2
and down/2
.
defmodule MyAPI.Changes.PostStatusChange do
use Versioning.Change
@desc """
The boolean field "active" was removed in favour of the enum "status".
"""
def down(versioning, _opts) do
case Versioning.pop_data(versioning, :status) do
{:active, versioning} -> Versioning.put_data(versioning, :active, true)
{_, versioning} -> Versioning.put_data(versioning, :active, false)
end
end
def up(versioning, _opts) do
case Versioning.pop_data(versioning, "active") do
{true, versioning} -> Versioning.put_data(versioning, "status", "active")
{false, versioning} -> Versioning.put_data(versioning, "status", "hidden")
{_, versioning} -> versioning
end
end
end
Our down/2
function accepts a versioning, removes the new :status
value, and
translates it to the old :active
requirements - returning a modified versioning.
Our up/2
function accepts a versioning, removes the old "active"
value, and
translates it to our new "status"
requirements - returning a modified versioning.
We can also use the @desc
module attribute to attach a description of the change.
This will be used when generating a changelog.
Versioning Schema
With our first change module in place, its time to tie it all together with our "versioning schema". The schema provides a DSL to describe and route our versioning.
defmodule MyAPI.Versioning do
use Versioning.Schema
version("2.0.0", do: [])
version "1.1.0" do
type "Post" do
change(MyAPI.Changes.PostStatusChange)
end
end
version("1.0.0", do: [])
end
The schema above shows we currently support 3 versions. Our top version "2.0.0"
represents the current version. "1.1.0"
is where our new article change is held.
The schema DSL describes a flow, whereby the "top" version represents the most recent,
and each subsequent version is one step older.
Running our Versioning
With our versioning in place, we can now translate our Post
struct to the requirements
of our users "pinned" API version.
#For the sake of example, lets say the user is pinned at the older "1.0.0" version.
version = get_api_version(user)
post = get_post(id)
versioning = Versioning.new(post, "2.0.0", version)
MyAPI.Versioning.run(versioning)
#Versioning<current: "2.0.0", target: "1.0.0", type: Post, changed: true>
Calling run/1
on our schema with a versioning struct will execute our schema
downwards/upwards depending on the order of our current and target versions. It
will "walk" through each version, running any changes held within it that match
the :type
on our versioning struct. A schema is typically run "downwards" when
converting local data to external data. A schema is typically run "updwards" when
converting external data to local data.
Once a matching version is found, it will run the changes within, but will stop execution afterwards.
We can then access the underlying data through our versioning.data
.
Versioning Changelog
A changelog of our schema can also be generated. This changelog represents a list of maps in the format (shortend for brevity):
[
%{
version: "2.0.0",
changes: []
},
%{
version: "1.1.0",
changes: [
%{
type: Post,
descriptions: [
"The boolean field `:active` was removed in favour of the enum `:status`."
]
}
]
},
%{
version: "1.0.0",
changes: []
}
]
You can access the changelog while providing options such as a formatter.
Included with Versioning
is a basic markdown formatter.
Versioning.Changelog.build(MyAPI.Versioning, formatter: Versioning.Changelog.Markdown)