Skip to content

Commit 61c7a46

Browse files
authored
Merge pull request #23 from redwirelabs/validation-metadata
Add validation metadata
2 parents 7087574 + 484fe7e commit 61c7a46

File tree

7 files changed

+771
-174
lines changed

7 files changed

+771
-174
lines changed

README.md

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,11 @@ Transform data when crossing layers. In the input handler, perform the message v
6767
```elixir
6868
def input_received(params) do
6969
case Speck.validate(MQTT.AddDevice.V1, params) do
70-
{:ok, message} ->
70+
{:ok, message, _meta} ->
7171
device = Device.create!(message.id, message.rs485_address)
7272
{:ok, device}
7373

74-
{:error, errors} ->
74+
{:error, errors, _meta} ->
7575
{:error, errors}
7676
end
7777
end
@@ -187,8 +187,8 @@ end
187187
```
188188

189189
```elixir
190-
with {:ok, shadow} <- Speck.validate(MQTT.AWS.Shadow.Update.V1, payload),
191-
{:ok, light_state} <- Speck.validate(MQTT.Light.State.V1, shadow.state.desired) do
190+
with {:ok, shadow, _meta} <- Speck.validate(MQTT.AWS.Shadow.Update.V1, payload),
191+
{:ok, light_state, _meta} <- Speck.validate(MQTT.Light.State.V1, shadow.state.desired) do
192192
# do something with light_state
193193
end
194194
```
@@ -234,3 +234,63 @@ name "add_device"
234234
attribute :id, :integer, strict: true
235235
attribute :name, :string
236236
```
237+
238+
## Validation metadata
239+
240+
In certain situations, only having the coerced data may not be enough, and you may need information about the input data as well. Since Speck has already traversed the input data, information about it is collected and returned as validation metadata. Speck provides `Speck.ValidationMetadata.Attribute` for working with metadata attributes that describe the input data structure.
241+
242+
### Device shadows
243+
244+
One scenario where Speck's metadata is helpful is when working with a device shadow, like in AWS IoT Core. This shadow uses a `desired` property for sending data to an embedded device, and `reported` for the device to report its current state. When validating one of these messages with Speck, the schema will not know about new desired properties if they are added in the cloud before the firmware is updated. Unfortunately, AWS will continue to send delta updates until these new desired properties are acknowledged in the reported properties.
245+
246+
In this case, the metadata can be captured along with the validated message.
247+
248+
```elixir
249+
{:ok, message, meta} <- Speck.validate(MQTT.AWS.Shadow.Update.V1, payload)
250+
```
251+
252+
The new, unknown fields will be filtered out of `message` during the validation process, as expected. This message is trusted data that should still be sent down to the business logic layer for processing. However, the unknown fields need to be reported to the device shadow to acknowledge them. This is where `meta` comes in.
253+
254+
Using `meta`, the unknown fields can be selected with a filter, and they can be merged into the `reported` map to send to the shadow.
255+
256+
```elixir
257+
reported = %{
258+
# Real data to report to the shadow ...
259+
}
260+
261+
meta
262+
|> Attribute.list
263+
|> Enum.filter(fn
264+
{_path, :unknown, _value} -> true
265+
{_path, _status, _value} -> false
266+
end)
267+
|> Attribute.merge(reported)
268+
```
269+
270+
### Fields as actions
271+
272+
Although typically fields in protocols carry data, sometimes their presence or a `nil` value signifies an action to perform. For example, setting a field `nil` signifies a delete (remove/cleanup) should be performed, versus the field not being present meaning there is no change. Speck's coercion process intentionally obscures this, since the primary goal is to normalize the input into a consistent data structure. This is another situation where Speck's metadata can be used to determine which fields are marked for deletion.
273+
274+
```elixir
275+
{:ok, message, meta} <- Speck.validate(DeltaMessage, payload)
276+
277+
attributes_to_delete =
278+
meta
279+
|> Attribute.list
280+
|> Enum.filter(fn
281+
{_path, :present, nil} -> true
282+
{_path, _status, _value} -> false
283+
end)
284+
|> Attribute.merge(%{})
285+
```
286+
287+
The resulting map can then be traversed in the business logic layer, performing removal actions for each component that a nil field represents.
288+
289+
```
290+
%{
291+
"device_is_installed" => nil,
292+
"remote_sensors" => [
293+
%{"temperature_sensor_1" => nil}
294+
]
295+
}
296+
```

0 commit comments

Comments
 (0)