Skip to content

Commit 586ab9f

Browse files
value + tests
Signed-off-by: Gabriele Ghio <gabriele.ghio@secomind.com>
1 parent 01a9717 commit 586ab9f

File tree

2 files changed

+261
-0
lines changed

2 files changed

+261
-0
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
#
2+
# This file is part of Astarte.
3+
#
4+
# Copyright 2025 SECO Mind Srl
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
19+
defmodule Astarte.Core.Generators.Mapping.Value do
20+
@moduledoc """
21+
This module provides generators for Interface values.
22+
"""
23+
use ExUnitProperties
24+
25+
alias Astarte.Core.Interface
26+
alias Astarte.Core.Mapping
27+
28+
alias Astarte.Core.Generators.Mapping.ValueType, as: ValueTypeGenerator
29+
30+
@doc """
31+
Generates a valid value based on interface
32+
"""
33+
@spec value(Interface.t()) :: StreamData.t(map())
34+
def value(%Interface{} = interface) when not is_struct(interface, StreamData) do
35+
interface |> constant() |> value()
36+
end
37+
38+
@spec value(StreamData.t(Interface.t())) :: StreamData.t(map())
39+
def value(gen), do: gen |> bind(&build_package/1)
40+
41+
defp build_package(%Interface{aggregation: :individual} = interface) do
42+
%Interface{mappings: mappings} = interface
43+
44+
gen all %Mapping{endpoint: endpoint, value_type: value_type} <- member_of(mappings),
45+
path <- endpoint_path(endpoint),
46+
value <- build_value(value_type) do
47+
%{path: path, value: value}
48+
end
49+
end
50+
51+
defp build_package(%Interface{aggregation: :object} = interface) do
52+
%Interface{mappings: [%Mapping{endpoint: endpoint} | _] = mappings} = interface
53+
54+
endpoint = endpoint |> String.split("/") |> Enum.drop(-1) |> Enum.join("/")
55+
56+
gen all path <- endpoint_path(endpoint),
57+
value <-
58+
mappings
59+
|> Map.new(fn %Mapping{endpoint: endpoint, value_type: value_type} ->
60+
{endpoint_postfix(endpoint), build_value(value_type)}
61+
end)
62+
|> optional_map() do
63+
%{path: path, value: value}
64+
end
65+
end
66+
67+
defp endpoint_path(endpoint) do
68+
endpoint
69+
|> String.split("/")
70+
|> Enum.map(&convert_token/1)
71+
|> fixed_list()
72+
|> map(&Enum.join(&1, "/"))
73+
end
74+
75+
defp endpoint_postfix(endpoint), do: Regex.replace(~r/.*\//, endpoint, "")
76+
77+
defp build_value(value_type), do: ValueTypeGenerator.value_from_type(value_type)
78+
79+
# Utilities
80+
defp convert_token(token) do
81+
case(Mapping.is_placeholder?(token)) do
82+
true -> string(:alphanumeric, min_length: 1)
83+
false -> constant(token)
84+
end
85+
end
86+
87+
@doc """
88+
Returns true if `path` matches `endpoint` according to the given `aggregation`.
89+
"""
90+
@spec path_matches_endpoint?(:individual | :object, String.t(), String.t()) :: boolean()
91+
def path_matches_endpoint?(:individual, endpoint, path),
92+
do:
93+
path_matches_endpoint?(
94+
endpoint |> Mapping.normalize_endpoint() |> String.split("/"),
95+
path |> String.split("/")
96+
)
97+
98+
def path_matches_endpoint?(:object, endpoint, path),
99+
do:
100+
path_matches_endpoint?(
101+
endpoint |> Mapping.normalize_endpoint() |> String.split("/") |> Enum.drop(-1),
102+
path |> String.split("/")
103+
)
104+
105+
defp path_matches_endpoint?([], []), do: true
106+
107+
defp path_matches_endpoint?(["" | endpoints], [_ | paths]),
108+
do: path_matches_endpoint?(endpoints, paths)
109+
110+
defp path_matches_endpoint?([same | endpoints], [same | paths]),
111+
do: path_matches_endpoint?(endpoints, paths)
112+
113+
defp path_matches_endpoint?(_, _), do: false
114+
end
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
#
2+
# This file is part of Astarte.
3+
#
4+
# Copyright 2025 SECO Mind Srl
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
19+
defmodule Astarte.Core.Generators.Mapping.ValueTest do
20+
use ExUnit.Case, async: true
21+
use ExUnitProperties
22+
23+
alias Astarte.Core.Interface
24+
alias Astarte.Core.Mapping
25+
alias Astarte.Core.Mapping.ValueType
26+
27+
alias Astarte.Core.Generators.Interface, as: InterfaceGenerator
28+
alias Astarte.Core.Generators.Mapping.Value, as: ValueGenerator
29+
30+
@moduletag :mapping
31+
@moduletag :value
32+
33+
@endpoints_path [
34+
{:individual, "/%{param}", "/abc", true},
35+
{:object, "/%{param}/a", "/dce", true},
36+
{:individual, "/Alpha/Beta/Gamma", "/Alpha/Beta/Gamma", true},
37+
{:individual, "/Alpha/Beta/Gamma", "/Alpha/Beta", false},
38+
{:individual, "/Shop/Prd45/Details", "/shop/Prd45/Details", false},
39+
{:individual, "/api/v1/users/%{UID}/orders/%{OID}/lines/LID",
40+
"/api/v1/users/U9/orders/O77/lines/LID", true},
41+
{:individual, "/api/v1/users/%{UID}/orders/%{OID}/lines/LID",
42+
"/api/v1/users/U9/orders/O77/lines", false},
43+
{:object, "/Catalog/%{Section}/Items", "/Catalog/Electronics", true},
44+
{:object, "/Catalog/%{Section}/Items", "/Catalog/Electronics/Items", false},
45+
{:object, "/Foo/%{Id}/Bar", "/Foo/A1", true},
46+
{:object, "/Foo/%{Id}/Bar", "/Foo/A1/B2", false},
47+
{:individual, "/User_Profile/%{UID123}/Orders/%{YEAR}/Summary",
48+
"/User_Profile/A7/Orders/2025/Summary", true},
49+
{:individual, "/User_Profile/%{UID123}/Orders/%{YEAR}/Summary",
50+
"/User_Profile/A7/Orders/2025", false},
51+
{:individual, "/Srv/%{Env}/Cfg/%{Key}/Apply", "/Srv/Prod/Cfg/DB1/Apply", true},
52+
{:individual, "/Srv/%{Env}/Cfg/%{Key}/Apply", "/Srv/Prod/Cfg/DB1/Apply/Now", false},
53+
{:individual, "/Root/%{A}/%{B}/%{C}/Leaf", "/Root/x1/y2/z3/Leaf", true},
54+
{:object, "/Root/%{A}/%{B}/%{C}/Leaf", "/Root/x1/y2/z3", true},
55+
{:object, "/Root/%{A}/%{B}/%{C}/Leaf", "/Root/x1/y2", false},
56+
{:individual, "/Alpha/%{X}/Beta/%{Y}/Gamma", "/Alpha/aa1/Beta/bb2/Gamma", true},
57+
{:individual, "/Alpha/%{X}/Beta/%{Y}/Gamma", "/Alpha/aa1/Beta/Gamma", false},
58+
{:individual, "/A_/B_/C_/D_/E_", "/A_/B_/C_/D_/E_", true},
59+
{:individual, "/A_/B_/C_/D_/E_", "/A_/B_/C_/D_", false},
60+
{:individual, "/calc/%{Mode}/run/Now", "/calc/Fast/run/Now", true},
61+
{:individual, "/calc/%{Mode}/run/Now", "/calc/Fast/run", false},
62+
{:object, "/calc/%{Mode}/run/Now", "/calc/Fast/run", true},
63+
{:individual, "/Auth/Login/Step/N", "/Auth/Login/Step/N", true},
64+
{:individual, "/Auth/Login/Step/N", "/Auth/Login/Step2", false},
65+
{:individual, "/node/%{N1}/leaf/%{L2}/x/K", "/node/N/leaf/L/x/K", true},
66+
{:object, "/node/%{N1}/leaf/%{L2}/x/K", "/node/N/leaf/L/x", true},
67+
{:individual, "/Lib/%{Pkg}/v/%{Major}/_/_Minor", "/Lib/core/v/1/_/_Minor", true},
68+
{:individual, "/Lib/%{Pkg}/v/%{Major}/_/_Minor", "/Lib/core/v/1", false},
69+
{:individual, "/xY/%{Za}/Q1/%{Rb}/Zz", "/xY/za1/Q1/rb2/Zz", true},
70+
{:individual, "/xY/%{Za}/Q1/%{Rb}/Zz", "/xY/za1/Q1/rb2/ZZ", false},
71+
{:object, "/alpha/%{A1}/beta/%{B2}/gamma", "/alpha/x1/beta/y2", true},
72+
{:object, "/alpha/%{A1}/beta/%{B2}/gamma", "/alpha/x1/beta", false},
73+
{:individual, "/srv/%{EnvX}/cfg/%{KeyX}/edit/User", "/srv/Dev/cfg/DB2/edit/User", true},
74+
{:individual, "/srv/%{EnvX}/cfg/%{KeyX}/edit/User", "/srv/Dev/cfg/DB2/edit", false},
75+
{:individual, "/AA/BB/CC/%{DD}/EE/FF", "/AA/BB/CC/d1/EE/FF", true},
76+
{:object, "/AA/BB/CC/%{DD}/EE/FF", "/AA/BB/CC/d1/EE", true},
77+
{:individual, "/Path/%{A}/To/%{B}/Res/%{C}/View", "/Path/A1/To/B2/Res/C3/View", true},
78+
{:individual, "/Path/%{A}/To/%{B}/Res/%{C}/View", "/Path/A1/To/B2/Res/C3/View/", false},
79+
{:individual, "/ROOT/%{X}/MID/%{Y}/TAIL/Z", "/ROOT/r1/MID/m2/TAIL/Z", true},
80+
{:object, "/ROOT/%{X}/MID/%{Y}/TAIL/Z", "/ROOT/r1/MID/m2/TAIL", true},
81+
{:individual, "/Ping/Pong/%{Who}/Score/Final", "/Ping/Pong/Alice/Score/Final", true},
82+
{:individual, "/Ping/Pong/%{Who}/Score/Final", "/Ping/Pong/Alice/ScoreFinal", false}
83+
]
84+
85+
@doc false
86+
describe "test utilities" do
87+
test "path_matches_endpoint?/3" do
88+
for {aggregation, endpoint, path, expected} <- @endpoints_path do
89+
assert expected == ValueGenerator.path_matches_endpoint?(aggregation, endpoint, path)
90+
end
91+
end
92+
end
93+
94+
@doc false
95+
describe "value generator" do
96+
@describetag :success
97+
@describetag :ut
98+
99+
property "generates value based on interface (gen)" do
100+
gen = InterfaceGenerator.interface() |> ValueGenerator.value()
101+
102+
check all value <- gen do
103+
assert %{path: _path, value: _value} = value
104+
end
105+
end
106+
107+
property "generates value based on interface (struct)" do
108+
check all interface <- InterfaceGenerator.interface(),
109+
value <- ValueGenerator.value(interface) do
110+
assert %{path: _path, value: _value} = value
111+
end
112+
end
113+
114+
property "generates value must have mapping path matches endpoint" do
115+
check all %Interface{mappings: mappings, aggregation: aggregation} = interface <-
116+
InterfaceGenerator.interface(),
117+
%{path: path, value: _value} <- ValueGenerator.value(interface) do
118+
assert Enum.any?(mappings, fn %Mapping{endpoint: endpoint} ->
119+
ValueGenerator.path_matches_endpoint?(aggregation, endpoint, path)
120+
end)
121+
end
122+
end
123+
124+
property "generates value must be valid type" do
125+
check all %Interface{mappings: mappings, aggregation: aggregation} = interface <-
126+
InterfaceGenerator.interface(),
127+
values <-
128+
ValueGenerator.value(interface)
129+
|> list_of(min_length: 1, max_length: 10) do
130+
for %{path: path, value: value} <- values do
131+
value =
132+
case {aggregation, value} do
133+
{:individual, value} -> value
134+
{:object, map} -> map |> Map.values() |> Enum.at(0)
135+
end
136+
137+
%Mapping{value_type: value_type} =
138+
Enum.find(mappings, fn %Mapping{endpoint: endpoint} ->
139+
ValueGenerator.path_matches_endpoint?(aggregation, endpoint, path)
140+
end)
141+
142+
assert ValueType.validate_value(value_type, value)
143+
end
144+
end
145+
end
146+
end
147+
end

0 commit comments

Comments
 (0)