Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Device Custom Attributes #205

Merged
merged 8 commits into from
Jun 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Add support for device tags ([#191](https://github.com/edgehog-device-manager/edgehog/pull/191))
- Add support for device custom attributes
([#205](https://github.com/edgehog-device-manager/edgehog/pull/205))

## [0.5.1] - 2022-06-01
### Added
Expand Down
193 changes: 193 additions & 0 deletions backend/lib/ecto/json_variant.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
#
# This file is part of Edgehog.
#
# Copyright 2022 SECO Mind Srl
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#

defmodule Ecto.JSONVariant do
use Ecto.Type

alias __MODULE__

# TODO: add support for array values
@supported_types [
:double,
:integer,
:boolean,
:longinteger,
:string,
:binaryblob,
:datetime
]

defstruct [:type, :value]

@impl true
def type, do: :map

@impl true
def cast(%{"type" => type, "value" => value}) when is_atom(type) and type in @supported_types do
do_cast(type, value)
end

def cast(%{"type" => type, "value" => value}) when is_binary(type) do
with {:ok, type} <- type_string_to_atom(type) do
do_cast(type, value)
end
end

def cast(%{type: type, value: value}) when is_atom(type) and type in @supported_types do
do_cast(type, value)
end

def cast(%{type: type, value: value}) when is_binary(type) do
with {:ok, type} <- type_string_to_atom(type) do
do_cast(type, value)
end
end

def cast(_) do
:error
end

defp do_cast(type, value) do
with {:ok, value} <- cast_fun(type).(value) do
{:ok, struct!(__MODULE__, type: type, value: value)}
end
end

defp cast_fun(:double), do: &Ecto.Type.cast(:float, &1)
defp cast_fun(:integer), do: &cast_integer/1
defp cast_fun(:boolean), do: &Ecto.Type.cast(:boolean, &1)
defp cast_fun(:longinteger), do: &cast_longinteger/1
defp cast_fun(:string), do: &cast_string/1
defp cast_fun(:binaryblob), do: &cast_binaryblob/1
defp cast_fun(:datetime), do: &Ecto.Type.cast(:utc_datetime_usec, &1)

defp cast_integer(term) when is_binary(term) do
case Integer.parse(term) do
{integer, ""} when abs(integer) <= 0x7FFF_FFFF -> {:ok, integer}
_ -> :error
end
end

defp cast_integer(term) when is_integer(term) and abs(term) <= 0x7FFF_FFFF, do: {:ok, term}
defp cast_integer(term) when is_integer(term), do: {:error, message: "is out of range"}
defp cast_integer(_), do: :error

defp cast_longinteger(term) when is_binary(term) do
case Integer.parse(term) do
{integer, ""} when abs(integer) <= 0x7FFF_FFFF_FFFF_FFFF -> {:ok, integer}
_ -> :error
end
end

defp cast_longinteger(term) when is_integer(term) and abs(term) <= 0x7FFF_FFFF_FFFF_FFFF do
{:ok, term}
end

defp cast_longinteger(_), do: :error

defp cast_string(term) when is_binary(term) do
if String.valid?(term) do
{:ok, term}
else
:error
end
end

defp cast_string(_), do: :error

defp cast_binaryblob(term) when is_binary(term) do
case Base.decode64(term) do
{:ok, value} -> {:ok, value}
_ -> :error
end
end

defp cast_binaryblob(_), do: :error

defp type_string_to_atom("double"), do: {:ok, :double}
defp type_string_to_atom("integer"), do: {:ok, :integer}
defp type_string_to_atom("boolean"), do: {:ok, :boolean}
defp type_string_to_atom("longinteger"), do: {:ok, :longinteger}
defp type_string_to_atom("string"), do: {:ok, :string}
defp type_string_to_atom("binaryblob"), do: {:ok, :binaryblob}
defp type_string_to_atom("datetime"), do: {:ok, :datetime}
defp type_string_to_atom(_), do: :error

@impl true
def dump(%JSONVariant{type: type, value: value}) when type in @supported_types do
with {:ok, value} <- dump_fun(type).(value) do
{:ok, %{t: Atom.to_string(type), v: value}}
end
end

def dump(_), do: :error

defp dump_fun(:double), do: &Ecto.Type.dump(:float, &1)
defp dump_fun(:integer), do: &Ecto.Type.dump(:integer, &1)
defp dump_fun(:boolean), do: &Ecto.Type.dump(:boolean, &1)
defp dump_fun(:longinteger), do: &Ecto.Type.dump(:integer, &1)
defp dump_fun(:string), do: &Ecto.Type.dump(:string, &1)
defp dump_fun(:binaryblob), do: &dump_binaryblob/1
defp dump_fun(:datetime), do: &dump_datetime/1

defp dump_binaryblob(value) do
with {:ok, binary} <- Ecto.Type.dump(:binary, value) do
{:ok, Base.encode64(binary)}
end
end

defp dump_datetime(value) do
with {:ok, datetime} <- Ecto.Type.dump(:utc_datetime_usec, value) do
{:ok, DateTime.to_iso8601(datetime)}
end
end

@impl true
def load(%{"t" => type_string, "v" => value}) do
with {:ok, type} <- type_string_to_atom(type_string),
{:ok, value} <- load_fun(type).(value) do
{:ok, struct!(__MODULE__, type: type, value: value)}
end
end

def load(_), do: :error

defp load_fun(:double), do: &Ecto.Type.load(:float, &1)
defp load_fun(:integer), do: &Ecto.Type.load(:integer, &1)
defp load_fun(:boolean), do: &Ecto.Type.load(:boolean, &1)
defp load_fun(:longinteger), do: &Ecto.Type.load(:integer, &1)
defp load_fun(:string), do: &Ecto.Type.load(:string, &1)
defp load_fun(:binaryblob), do: &load_binaryblob/1
defp load_fun(:datetime), do: &load_datetime/1

defp load_binaryblob(value) do
case Base.decode64(value) do
{:ok, binary} -> Ecto.Type.load(:binary, binary)
_ -> :error
end
end

defp load_datetime(value) do
case DateTime.from_iso8601(value) do
{:ok, datetime, 0} -> Ecto.Type.load(:utc_datetime_usec, datetime)
_ -> :error
end
end
end
10 changes: 5 additions & 5 deletions backend/lib/edgehog/astarte.ex
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ defmodule Edgehog.Astarte do
filters
|> Enum.reduce(Device, &filter_with/2)
|> Repo.all()
|> Repo.preload(:tags)
|> Repo.preload([:tags, :custom_attributes])
end

defp filter_with(filter, query) do
Expand Down Expand Up @@ -487,7 +487,7 @@ defmodule Edgehog.Astarte do
"""
def get_device!(id) do
Repo.get!(Device, id)
|> Repo.preload(:tags)
|> Repo.preload([:tags, :custom_attributes])
end

@doc """
Expand All @@ -508,7 +508,7 @@ defmodule Edgehog.Astarte do
|> Device.changeset(attrs)

with {:ok, device} <- Repo.insert(changeset) do
{:ok, Repo.preload(device, :tags)}
{:ok, Repo.preload(device, [:tags, :custom_attributes])}
end
end

Expand Down Expand Up @@ -555,7 +555,7 @@ defmodule Edgehog.Astarte do
|> Repo.transaction()
|> case do
{:ok, %{update_device: device}} ->
{:ok, Repo.preload(device, :tags)}
{:ok, Repo.preload(device, [:tags, :custom_attributes])}

{:error, _failed_operation, failed_value, _progress_so_far} ->
{:error, failed_value}
Expand Down Expand Up @@ -605,7 +605,7 @@ defmodule Edgehog.Astarte do
"""
def fetch_realm_device(%Realm{id: realm_id}, device_id) do
case Repo.get_by(Device, realm_id: realm_id, device_id: device_id) do
%Device{} = device -> {:ok, Repo.preload(device, :tags)}
%Device{} = device -> {:ok, Repo.preload(device, [:tags, :custom_attributes])}
nil -> {:error, :device_not_found}
end
end
Expand Down
5 changes: 5 additions & 0 deletions backend/lib/edgehog/astarte/device.ex
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ defmodule Edgehog.Astarte.Device do
has_one :system_model, through: [:system_model_part_number, :system_model]
many_to_many :tags, Devices.Tag, join_through: Devices.DeviceTag, on_replace: :delete

has_many :custom_attributes, Devices.Attribute,
where: [namespace: "custom"],
on_replace: :delete

timestamps()
end

Expand Down Expand Up @@ -74,5 +78,6 @@ defmodule Edgehog.Astarte.Device do
:part_number
])
|> validate_required([:name])
|> cast_assoc(:custom_attributes, with: &Devices.Attribute.custom_attribute_changeset/2)
end
end
52 changes: 52 additions & 0 deletions backend/lib/edgehog/devices/attribute.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#
# This file is part of Edgehog.
#
# Copyright 2022 SECO Mind Srl
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#

defmodule Edgehog.Devices.Attribute do
use Ecto.Schema
import Ecto.Changeset

@primary_key false
schema "device_attributes" do
field :tenant_id, :integer,
autogenerate: {Edgehog.Repo, :get_tenant_id, []},
primary_key: true

field :device_id, :id, primary_key: true
field :namespace, Ecto.Enum, values: [:custom], primary_key: true
field :key, :string, primary_key: true
field :typed_value, Ecto.JSONVariant

timestamps()
end

@doc false
def changeset(attributes, attrs) do
attributes
|> cast(attrs, [:namespace, :key, :typed_value])
|> validate_required([:namespace, :key, :typed_value])
|> validate_format(:key, ~r/[a-z0-9-_]+/)
end

@doc false
def custom_attribute_changeset(attributes, attrs) do
changeset(attributes, attrs)
|> validate_inclusion(:namespace, [:custom])
end
end
25 changes: 25 additions & 0 deletions backend/lib/edgehog_web/resolvers/astarte.ex
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ defmodule EdgehogWeb.Resolvers.Astarte do

def update_device(%{device_id: id} = attrs, %{context: context}) do
device = Astarte.get_device!(id)
attrs = maybe_wrap_typed_values(attrs)

with {:ok, device} <- Astarte.update_device(device, attrs) do
device = preload_system_model_for_device(device, context)
Expand Down Expand Up @@ -247,4 +248,28 @@ defmodule EdgehogWeb.Resolvers.Astarte do
_ -> {:error, "Unknown led behavior"}
end
end

defp maybe_wrap_typed_values(%{custom_attributes: custom_attributes} = attrs)
when is_list(custom_attributes) do
wrapped_attributes =
Enum.map(custom_attributes, fn attr ->
%{
namespace: namespace,
key: key,
type: type,
value: value
} = attr

# Wrap type and value under the :typed_value key, as expected by the Ecto schema
%{
namespace: namespace,
key: key,
typed_value: %{type: type, value: value}
}
end)

%{attrs | custom_attributes: wrapped_attributes}
end

defp maybe_wrap_typed_values(attrs), do: attrs
end
11 changes: 11 additions & 0 deletions backend/lib/edgehog_web/resolvers/devices.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@
defmodule EdgehogWeb.Resolvers.Devices do
alias Edgehog.Astarte
alias Edgehog.Devices
alias Edgehog.Devices.Attribute
alias Edgehog.Devices.HardwareType
alias Edgehog.Devices.SystemModel
alias EdgehogWeb.Schema.VariantTypes

def find_hardware_type(%{id: id}, _resolution) do
Devices.fetch_hardware_type(id)
Expand Down Expand Up @@ -174,4 +176,13 @@ defmodule EdgehogWeb.Resolvers.Devices do
tag_names = for t <- tags, do: t.name
{:ok, tag_names}
end

def extract_attribute_type(%Attribute{typed_value: typed_value}, _args, _context) do
{:ok, typed_value.type}
end

def extract_attribute_value(%Attribute{typed_value: typed_value}, _args, _context) do
%Ecto.JSONVariant{type: type, value: value} = typed_value
VariantTypes.encode_variant_value(type, value)
end
end
1 change: 1 addition & 0 deletions backend/lib/edgehog_web/schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ defmodule EdgehogWeb.Schema do
import_types EdgehogWeb.Schema.LocalizationTypes
import_types EdgehogWeb.Schema.OSManagementTypes
import_types EdgehogWeb.Schema.TenantsTypes
import_types EdgehogWeb.Schema.VariantTypes
import_types Absinthe.Plug.Types
import_types Absinthe.Type.Custom

Expand Down
Loading