Skip to content
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
114 changes: 114 additions & 0 deletions lib/astarte/core/generators/mapping/value.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#
# This file is part of Astarte.
#
# Copyright 2025 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.
#

defmodule Astarte.Core.Generators.Mapping.Value do
@moduledoc """
This module provides generators for Interface values.
"""
use ExUnitProperties

alias Astarte.Core.Interface
alias Astarte.Core.Mapping

alias Astarte.Core.Generators.Mapping.ValueType, as: ValueTypeGenerator

@doc """
Generates a valid value based on interface
"""
@spec value(Interface.t()) :: StreamData.t(map())
def value(%Interface{} = interface) when not is_struct(interface, StreamData) do
interface |> constant() |> value()
end

@spec value(StreamData.t(Interface.t())) :: StreamData.t(map())
def value(gen), do: gen |> bind(&build_package/1)

defp build_package(%Interface{aggregation: :individual} = interface) do
%Interface{mappings: mappings} = interface

gen all %Mapping{endpoint: endpoint, value_type: value_type} <- member_of(mappings),
path <- endpoint_path(endpoint),
value <- build_value(value_type) do
%{path: path, value: value}
end
end

defp build_package(%Interface{aggregation: :object} = interface) do
%Interface{mappings: [%Mapping{endpoint: endpoint} | _] = mappings} = interface

endpoint = endpoint |> String.split("/") |> Enum.drop(-1) |> Enum.join("/")

gen all path <- endpoint_path(endpoint),
value <-
mappings
|> Map.new(fn %Mapping{endpoint: endpoint, value_type: value_type} ->
{endpoint_postfix(endpoint), build_value(value_type)}
end)
|> optional_map() do
%{path: path, value: value}
end
end

defp endpoint_path(endpoint) do
endpoint
|> String.split("/")
|> Enum.map(&convert_token/1)
|> fixed_list()
|> map(&Enum.join(&1, "/"))
end

defp endpoint_postfix(endpoint), do: Regex.replace(~r/.*\//, endpoint, "")

defp build_value(value_type), do: ValueTypeGenerator.value_from_type(value_type)

# Utilities
defp convert_token(token) do
case(Mapping.is_placeholder?(token)) do
true -> string(:alphanumeric, min_length: 1)
false -> constant(token)
end
end

@doc """
Returns true if `path` matches `endpoint` according to the given `aggregation`.
"""
@spec path_matches_endpoint?(:individual | :object, String.t(), String.t()) :: boolean()
def path_matches_endpoint?(:individual, endpoint, path),
do:
path_matches_endpoint?(
endpoint |> Mapping.normalize_endpoint() |> String.split("/"),
path |> String.split("/")
)

def path_matches_endpoint?(:object, endpoint, path),
do:
path_matches_endpoint?(
endpoint |> Mapping.normalize_endpoint() |> String.split("/") |> Enum.drop(-1),
path |> String.split("/")
)

defp path_matches_endpoint?([], []), do: true

defp path_matches_endpoint?(["" | endpoints], [_ | paths]),
do: path_matches_endpoint?(endpoints, paths)

defp path_matches_endpoint?([same | endpoints], [same | paths]),
do: path_matches_endpoint?(endpoints, paths)

defp path_matches_endpoint?(_, _), do: false
end
88 changes: 88 additions & 0 deletions lib/astarte/core/generators/mapping/value_type.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#
# This file is part of Astarte.
#
# Copyright 2025 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.
#

defmodule Astarte.Core.Generators.Mapping.ValueType do
@moduledoc """
This module provides generators for any ValueType.
"""
use ExUnitProperties

alias Astarte.Core.Mapping.ValueType

alias Astarte.Common.Generators.DateTime, as: DateTimeGenerator

@valid_atoms [
:double,
:integer,
:boolean,
:longinteger,
:string,
:binaryblob,
:datetime,
:doublearray,
:integerarray,
:booleanarray,
:longintegerarray,
:stringarray,
:binaryblobarray,
:datetimearray
]

@type valid_t ::
unquote(
@valid_atoms
|> Enum.map_join(" | ", &inspect/1)
|> Code.string_to_quoted!()
)

@doc """
List of all astarte's ValueType atoms
"""
@spec valid_atoms() :: list(atom())
def valid_atoms, do: @valid_atoms

@doc """
Generates a valid ValueType
"""
@spec value_type() :: StreamData.t(ValueType.t())
def value_type, do: member_of(valid_atoms())

@doc """
Generates a valid value from ValueType
"""
@spec value_from_type(type :: valid_t()) :: StreamData.t(any())
def value_from_type(:double), do: float()
def value_from_type(:integer), do: integer(-0x7FFFFFFF..0x7FFFFFFF)
def value_from_type(:boolean), do: boolean()
def value_from_type(:longinteger), do: integer(-0x7FFFFFFFFFFFFFFF..0x7FFFFFFFFFFFFFFF)
def value_from_type(:string), do: string(:utf8, max_length: 65_535)
def value_from_type(:binaryblob), do: binary(max_length: 65_535)

def value_from_type(:datetime), do: DateTimeGenerator.date_time()

def value_from_type(array) when is_atom(array),
do: type_array(array) |> value_from_type() |> list_of(max_length: 1023)

defp type_array(:doublearray), do: :double
defp type_array(:integerarray), do: :integer
defp type_array(:longintegerarray), do: :longinteger
defp type_array(:booleanarray), do: :boolean
defp type_array(:stringarray), do: :string
defp type_array(:binaryblobarray), do: :binaryblob
defp type_array(:datetimearray), do: :datetime
end
142 changes: 142 additions & 0 deletions test/astarte/core/generators/mapping/value_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#
# This file is part of Astarte.
#
# Copyright 2025 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.
#

defmodule Astarte.Core.Generators.Mapping.ValueTest do
use ExUnit.Case, async: true
use ExUnitProperties

alias Astarte.Core.Interface
alias Astarte.Core.Mapping
alias Astarte.Core.Mapping.ValueType

alias Astarte.Core.Generators.Interface, as: InterfaceGenerator
alias Astarte.Core.Generators.Mapping.Value, as: ValueGenerator

@moduletag :mapping
@moduletag :value

@endpoints_path [
{:individual, "/%{param}", "/abc", true},
{:object, "/%{param}/a", "/dce", true},
{:individual, "/Alpha/Beta/Gamma", "/Alpha/Beta/Gamma", true},
{:individual, "/Alpha/Beta/Gamma", "/Alpha/Beta", false},
{:individual, "/Shop/Prd45/Details", "/shop/Prd45/Details", false},
{:individual, "/api/v1/users/%{UID}/orders/%{OID}/lines/LID",
"/api/v1/users/U9/orders/O77/lines/LID", true},
{:individual, "/api/v1/users/%{UID}/orders/%{OID}/lines/LID",
"/api/v1/users/U9/orders/O77/lines", false},
{:object, "/Catalog/%{Section}/Items", "/Catalog/Electronics", true},
{:object, "/Catalog/%{Section}/Items", "/Catalog/Electronics/Items", false},
{:object, "/Foo/%{Id}/Bar", "/Foo/A1", true},
{:object, "/Foo/%{Id}/Bar", "/Foo/A1/B2", false},
{:individual, "/User_Profile/%{UID123}/Orders/%{YEAR}/Summary",
"/User_Profile/A7/Orders/2025/Summary", true},
{:individual, "/User_Profile/%{UID123}/Orders/%{YEAR}/Summary",
"/User_Profile/A7/Orders/2025", false},
{:individual, "/Srv/%{Env}/Cfg/%{Key}/Apply", "/Srv/Prod/Cfg/DB1/Apply", true},
{:individual, "/Srv/%{Env}/Cfg/%{Key}/Apply", "/Srv/Prod/Cfg/DB1/Apply/Now", false},
{:individual, "/Root/%{A}/%{B}/%{C}/Leaf", "/Root/x1/y2/z3/Leaf", true},
{:object, "/Root/%{A}/%{B}/%{C}/Leaf", "/Root/x1/y2/z3", true},
{:object, "/Root/%{A}/%{B}/%{C}/Leaf", "/Root/x1/y2", false},
{:individual, "/Alpha/%{X}/Beta/%{Y}/Gamma", "/Alpha/aa1/Beta/bb2/Gamma", true},
{:individual, "/Alpha/%{X}/Beta/%{Y}/Gamma", "/Alpha/aa1/Beta/Gamma", false},
{:individual, "/A_/B_/C_/D_/E_", "/A_/B_/C_/D_/E_", true},
{:individual, "/A_/B_/C_/D_/E_", "/A_/B_/C_/D_", false},
{:individual, "/calc/%{Mode}/run/Now", "/calc/Fast/run/Now", true},
{:individual, "/calc/%{Mode}/run/Now", "/calc/Fast/run", false},
{:object, "/calc/%{Mode}/run/Now", "/calc/Fast/run", true},
{:individual, "/Auth/Login/Step/N", "/Auth/Login/Step/N", true},
{:individual, "/Auth/Login/Step/N", "/Auth/Login/Step2", false},
{:individual, "/node/%{N1}/leaf/%{L2}/x/K", "/node/N/leaf/L/x/K", true},
{:object, "/node/%{N1}/leaf/%{L2}/x/K", "/node/N/leaf/L/x", true},
{:individual, "/Lib/%{Pkg}/v/%{Major}/_/_Minor", "/Lib/core/v/1/_/_Minor", true},
{:individual, "/Lib/%{Pkg}/v/%{Major}/_/_Minor", "/Lib/core/v/1", false},
{:individual, "/xY/%{Za}/Q1/%{Rb}/Zz", "/xY/za1/Q1/rb2/Zz", true},
{:individual, "/xY/%{Za}/Q1/%{Rb}/Zz", "/xY/za1/Q1/rb2/ZZ", false},
{:object, "/alpha/%{A1}/beta/%{B2}/gamma", "/alpha/x1/beta/y2", true},
{:object, "/alpha/%{A1}/beta/%{B2}/gamma", "/alpha/x1/beta", false},
{:individual, "/srv/%{EnvX}/cfg/%{KeyX}/edit/User", "/srv/Dev/cfg/DB2/edit/User", true},
{:individual, "/srv/%{EnvX}/cfg/%{KeyX}/edit/User", "/srv/Dev/cfg/DB2/edit", false},
{:individual, "/AA/BB/CC/%{DD}/EE/FF", "/AA/BB/CC/d1/EE/FF", true},
{:object, "/AA/BB/CC/%{DD}/EE/FF", "/AA/BB/CC/d1/EE", true},
{:individual, "/Path/%{A}/To/%{B}/Res/%{C}/View", "/Path/A1/To/B2/Res/C3/View", true},
{:individual, "/Path/%{A}/To/%{B}/Res/%{C}/View", "/Path/A1/To/B2/Res/C3/View/", false},
{:individual, "/ROOT/%{X}/MID/%{Y}/TAIL/Z", "/ROOT/r1/MID/m2/TAIL/Z", true},
{:object, "/ROOT/%{X}/MID/%{Y}/TAIL/Z", "/ROOT/r1/MID/m2/TAIL", true},
{:individual, "/Ping/Pong/%{Who}/Score/Final", "/Ping/Pong/Alice/Score/Final", true},
{:individual, "/Ping/Pong/%{Who}/Score/Final", "/Ping/Pong/Alice/ScoreFinal", false}
]

defp value_to_check(:individual, value), do: value
defp value_to_check(:object, value) when is_map(value), do: value |> Map.values() |> Enum.at(0)

@doc false
describe "test utilities" do
test "path_matches_endpoint?/3" do
for {aggregation, endpoint, path, expected} <- @endpoints_path do
assert expected == ValueGenerator.path_matches_endpoint?(aggregation, endpoint, path)
end
end
end

@doc false
describe "value generator" do
@describetag :success
@describetag :ut

property "generates value based on interface (gen)" do
gen = InterfaceGenerator.interface() |> ValueGenerator.value()

check all value <- gen do
assert %{path: _path, value: _value} = value
end
end

property "generates value based on interface (struct)" do
check all interface <- InterfaceGenerator.interface(),
value <- ValueGenerator.value(interface) do
assert %{path: _path, value: _value} = value
end
end

property "generates value must have mapping path matches endpoint" do
check all %Interface{mappings: mappings, aggregation: aggregation} = interface <-
InterfaceGenerator.interface(),
%{path: path, value: _value} <- ValueGenerator.value(interface) do
assert Enum.any?(mappings, fn %Mapping{endpoint: endpoint} ->
ValueGenerator.path_matches_endpoint?(aggregation, endpoint, path)
end)
end
end

property "generates value must be valid type" do
check all %Interface{mappings: mappings, aggregation: aggregation} = interface <-
InterfaceGenerator.interface(),
%{path: path, value: value} <- ValueGenerator.value(interface) do
value = value_to_check(aggregation, value)

%Mapping{value_type: value_type} =
Enum.find(mappings, fn %Mapping{endpoint: endpoint} ->
ValueGenerator.path_matches_endpoint?(aggregation, endpoint, path)
end)

assert ValueType.validate_value(value_type, value)
end
end
end
end
Loading
Loading