Skip to content

Commit eeefbfc

Browse files
Merge pull request #65 from shinnokdisengir/fix/individual-mapping-interface
fix: aggregation individual are now more diversified + test on :object
2 parents 29224e7 + c89b06c commit eeefbfc

File tree

3 files changed

+185
-48
lines changed

3 files changed

+185
-48
lines changed

lib/astarte/core/generators/interface.ex

Lines changed: 82 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,14 @@ defmodule Astarte.Core.Generators.Interface do
2727

2828
alias Astarte.Core.CQLUtils
2929
alias Astarte.Core.Interface
30+
alias Astarte.Core.Mapping
3031

3132
alias Astarte.Core.Generators.Mapping, as: MappingGenerator
3233

3334
alias Astarte.Utilities.Map, as: MapUtilities
3435

36+
@interface_max_mappings 10
37+
3538
@doc """
3639
Generates a valid Astarte Interface.
3740
@@ -46,8 +49,7 @@ defmodule Astarte.Core.Generators.Interface do
4649
type <- type(),
4750
ownership <- ownership(),
4851
aggregation <- aggregation(type),
49-
prefix <- prefix(),
50-
mappings <- mappings(type, name, major_version, prefix),
52+
mappings <- mappings(aggregation, type, name, major_version),
5153
description <- description(),
5254
doc <- doc(),
5355
params: params do
@@ -59,7 +61,6 @@ defmodule Astarte.Core.Generators.Interface do
5961
minor_version: minor_version,
6062
type: type,
6163
ownership: ownership,
62-
prefix: prefix,
6364
aggregation: aggregation,
6465
mappings: mappings,
6566
description: description,
@@ -131,7 +132,7 @@ defmodule Astarte.Core.Generators.Interface do
131132
132133
https://docs.astarte-platform.org/astarte/latest/030-interface.html#interface-type
133134
"""
134-
@spec type() :: StreamData.t(String.t())
135+
@spec type() :: StreamData.t(:datastream | :properties)
135136
def type, do: member_of([:datastream, :properties])
136137

137138
@doc false
@@ -182,40 +183,89 @@ defmodule Astarte.Core.Generators.Interface do
182183

183184
defp ownership, do: member_of([:device, :server])
184185

185-
defp prefix, do: MappingGenerator.endpoint()
186+
defp interface_mapping(params, endpoint),
187+
do: MappingGenerator.mapping(params ++ [endpoint: endpoint])
186188

187-
defp mappings(interface_type, interface_name, interface_major, prefix) do
188-
gen all retention <- MappingGenerator.retention(interface_type),
189-
reliability <- MappingGenerator.reliability(interface_type),
190-
expiry <- MappingGenerator.expiry(interface_type),
191-
allow_unset <- MappingGenerator.allow_unset(interface_type),
192-
explicit_timestamp <- MappingGenerator.explicit_timestamp(interface_type),
193-
mappings <-
194-
MappingGenerator.endpoint_segment()
195-
|> list_of(min_length: 1, max_length: 10)
196-
|> map(&Enum.uniq_by(&1, fn endpoint -> String.downcase(endpoint) end))
197-
|> bind(fn list ->
198-
list
199-
|> Enum.map(fn postfix ->
200-
MappingGenerator.mapping(
201-
interface_type: interface_type,
202-
interface_name: interface_name,
203-
interface_major: interface_major,
204-
retention: retention,
205-
reliability: reliability,
206-
expiry: expiry,
207-
allow_unset: allow_unset,
208-
explicit_timestamp: explicit_timestamp,
209-
endpoint: prefix <> "/" <> postfix
210-
)
211-
end)
212-
|> fixed_list()
213-
end) do
189+
defp interface_mappings(:individual, params) do
190+
MappingGenerator.endpoint()
191+
|> bind(&interface_mapping(params, &1))
192+
|> list_of(min_length: 1, max_length: @interface_max_mappings)
193+
|> map(&uniq_endpoints/1)
194+
end
195+
196+
defp interface_mappings(:object, params) do
197+
MappingGenerator.endpoint()
198+
|> bind(fn endpoint ->
199+
MappingGenerator.endpoint_segment()
200+
|> bind(&interface_mapping(params, endpoint <> &1))
201+
|> list_of(min_length: 1, max_length: @interface_max_mappings)
202+
|> map(&uniq_endpoints/1)
203+
end)
204+
end
205+
206+
defp mappings(aggregation, interface_type, interface_name, interface_major) do
207+
common =
208+
gen all retention <- MappingGenerator.retention(interface_type),
209+
reliability <- MappingGenerator.reliability(interface_type),
210+
expiry <- MappingGenerator.expiry(interface_type),
211+
allow_unset <- MappingGenerator.allow_unset(interface_type),
212+
explicit_timestamp <- MappingGenerator.explicit_timestamp(interface_type) do
213+
[
214+
retention: retention,
215+
reliability: reliability,
216+
expiry: expiry,
217+
allow_unset: allow_unset,
218+
explicit_timestamp: explicit_timestamp
219+
]
220+
end
221+
222+
gen all common_params <- common,
223+
other_params = [
224+
interface_type: interface_type,
225+
interface_name: interface_name,
226+
interface_major: interface_major
227+
],
228+
params = common_params ++ other_params,
229+
mappings <- interface_mappings(aggregation, params) do
214230
mappings
215231
end
216232
end
217233

218234
defp description, do: one_of([nil, string(:ascii, min_length: 1, max_length: 1000)])
219235

220236
defp doc, do: one_of([nil, string(:ascii, min_length: 1, max_length: 100_000)])
237+
238+
# Utilities
239+
240+
@normalized_param ""
241+
242+
@doc false
243+
@spec uniq_endpoints(list(Mapping.t())) :: list(Mapping.t())
244+
def uniq_endpoints([%Mapping{} | _] = mappings),
245+
do: mappings |> uniq_endpoints(MapSet.new(), MapSet.new())
246+
247+
defp uniq_endpoints([], _endpoints, acc), do: MapSet.to_list(acc)
248+
249+
defp uniq_endpoints([%Mapping{endpoint: endpoint} = mapping | rest], prefixes, acc) do
250+
prefix = endpoint |> uniform_param() |> tokenize_endpoint()
251+
252+
if conflict_exists?(prefixes, prefix) do
253+
uniq_endpoints(rest, prefixes, acc)
254+
else
255+
uniq_endpoints(rest, MapSet.put(prefixes, prefix), MapSet.put(acc, mapping))
256+
end
257+
end
258+
259+
defp tokenize_endpoint(endpoint), do: endpoint |> String.split("/") |> Enum.drop(1)
260+
261+
defp conflict?([], _), do: true
262+
defp conflict?(_, []), do: true
263+
defp conflict?([@normalized_param | ta], [_ | tb]), do: conflict?(ta, tb)
264+
defp conflict?([_ | ta], [@normalized_param | tb]), do: conflict?(ta, tb)
265+
defp conflict?([h | ta], [h | tb]), do: conflict?(ta, tb)
266+
defp conflict?(_, _), do: false
267+
268+
defp conflict_exists?(prefixes, test), do: Enum.any?(prefixes, &conflict?(&1, test))
269+
270+
defp uniform_param(endpoint), do: Mapping.normalize_endpoint(endpoint) |> String.downcase()
221271
end

lib/astarte/core/generators/mapping.ex

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -101,34 +101,35 @@ defmodule Astarte.Core.Generators.Mapping do
101101
"""
102102
@spec endpoint() :: StreamData.t(String.t())
103103
def endpoint do
104-
gen all prefix <- endpoint_segment(),
105-
segments <-
104+
gen all segments <-
106105
frequency([
107106
{3, endpoint_segment()},
108107
{1, endpoint_segment_param()}
109108
])
110109
|> list_of(min_length: 1, max_length: 5) do
111-
"/" <> prefix <> "/" <> Enum.join(segments, "/")
110+
Enum.join(segments, "")
112111
end
113112
end
114113

115114
@doc """
116115
Generates a generic endpoint segment.
117116
"""
118117
@spec endpoint_segment() :: StreamData.t(StreamData.t(String.t()))
119-
def endpoint_segment do
120-
gen all prefix <- string(@unix_prefix_path_chars, length: 1),
121-
rest <- string(@unix_path_chars, max_length: 19) do
122-
prefix <> rest
123-
end
124-
end
118+
def endpoint_segment, do: endpoint_segment_content() |> map(fn content -> "/" <> content end)
125119

126120
@doc """
127121
Generates a parametrized endpoint segment.
128122
"""
129123
@spec endpoint_segment_param() :: StreamData.t(StreamData.t(String.t()))
130124
def endpoint_segment_param,
131-
do: endpoint_segment() |> map(fn segment -> "%{" <> segment <> "}" end)
125+
do: endpoint_segment_content() |> map(fn content -> "/%{" <> content <> "}" end)
126+
127+
defp endpoint_segment_content do
128+
gen all prefix <- string(@unix_prefix_path_chars, length: 1),
129+
rest <- string(@unix_path_chars, max_length: 19) do
130+
prefix <> rest
131+
end
132+
end
132133

133134
defp endpoint_id(interface_name, interface_major, endpoint),
134135
do: constant(CQLUtils.endpoint_id(interface_name, interface_major, endpoint))
@@ -154,12 +155,12 @@ defmodule Astarte.Core.Generators.Mapping do
154155

155156
@doc false
156157
@spec reliability(:datastream | :properties) ::
157-
StreamData.t(:unreliable | :guaranteed | :unique)
158+
StreamData.t(:unreliable | :guaranteed | :unique | nil)
158159
def reliability(:datastream), do: member_of([:unreliable, :guaranteed, :unique])
159160
def reliability(_), do: constant(nil)
160161

161162
@doc false
162-
@spec explicit_timestamp(:datastream | :properties) :: StreamData.t(nil | boolean())
163+
@spec explicit_timestamp(:datastream | :properties) :: StreamData.t(boolean())
163164
def explicit_timestamp(:datastream), do: boolean()
164165
def explicit_timestamp(_), do: constant(false)
165166

@@ -169,18 +170,19 @@ defmodule Astarte.Core.Generators.Mapping do
169170
def retention(_), do: constant(:discard)
170171

171172
@doc false
172-
@spec expiry(:datastream | :properties) :: StreamData.t(0 | pos_integer())
173+
@spec expiry(:datastream | :properties) :: StreamData.t(pos_integer() | 0)
173174
def expiry(:datastream), do: one_of([constant(0), integer(1..10_000)])
174175
def expiry(_), do: constant(0)
175176

176177
@doc false
177-
@spec database_retention_policy(:datastream | :properties) :: StreamData.t(:no_ttl | :use_ttl)
178+
@spec database_retention_policy(:datastream | :properties) ::
179+
StreamData.t(:no_ttl | :use_ttl | nil)
178180
def database_retention_policy(:datastream), do: member_of([:no_ttl, :use_ttl])
179181
def database_retention_policy(_), do: constant(nil)
180182

181183
@doc false
182184
@spec database_retention_ttl(:datastream | :properties, :use_ttl | :no_ttl) ::
183-
StreamData.t(nil | non_neg_integer())
185+
StreamData.t(non_neg_integer() | nil)
184186
def database_retention_ttl(:datastream, :use_ttl), do: integer(60..1_048_576)
185187
def database_retention_ttl(_, _), do: constant(nil)
186188

test/astarte/core/generators/interface_test.exs

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,68 @@ defmodule Astarte.Core.Generators.InterfaceTest do
2323
use ExUnit.Case, async: true
2424
use ExUnitProperties
2525

26-
alias Astarte.Core.Generators.Interface, as: InterfaceGenerator
2726
alias Astarte.Core.Interface
27+
alias Astarte.Core.Mapping
28+
29+
alias Astarte.Core.Generators.Interface, as: InterfaceGenerator
2830

2931
@moduletag :core
3032
@moduletag :interface
3133

34+
@endpoint_cases [
35+
{["/AbCde", "/AbCde/QwEr", "/XyZ"], ["/AbCde", "/XyZ"]},
36+
{["/shop/Prd45", "/shop/Prd45/Details"], ["/shop/Prd45"]},
37+
{["/alpha/Beta", "/alpha/Beta/Gamma", "/alpha/Delta"], ["/alpha/Beta", "/alpha/Delta"]},
38+
{["/%{RootB}/Xy", "/%{RootA}", "/%{RootA}/Xy", "/Abc"], ["/%{RootB}/Xy"]},
39+
{["/%{RootB}", "/%{RootB}/Seg1", "/%{RootB}/Seg1/Seg2"], ["/%{RootB}"]},
40+
{["/api/%{CA}", "/api/%{CA}/status", "/api/%{VA}/status"], ["/api/%{CA}"]},
41+
{["/api/%{V_1}", "/api/%{V_1}/ping", "/api/%{V_2}"], ["/api/%{V_1}"]},
42+
{["/api/%{V_1}/detail", "/%{mask}/detail", "/api/%{V_2}"], ["/api/%{V_1}/detail"]},
43+
{["/Ux9/Za2", "/Ux9/Za2/Rt7"], ["/Ux9/Za2"]},
44+
{["/foo/%{idA}", "/foo/%{idB}/bar"], ["/foo/%{idA}"]},
45+
{["/foo/%{Ab1}", "/foo/%{Ab1}/X", "/bar/Baz"], ["/foo/%{Ab1}", "/bar/Baz"]},
46+
{["/mmNn/ooPP", "/mmNn/ooPP/qqRR"], ["/mmNn/ooPP"]},
47+
{["/docs/%{Slug_1}", "/docs/%{Slug_1}/v1", "/docs/%{Slug_2}"], ["/docs/%{Slug_1}"]},
48+
{["/AA/BB", "/AA/BB/CC", "/DD"], ["/AA/BB", "/DD"]},
49+
{["/AA/BB/CC", "/AA/BB"], ["/AA/BB/CC"]},
50+
{["/xY/Za", "/xY/Za/Q1", "/xY/Zb"], ["/xY/Za", "/xY/Zb"]},
51+
{["/p1q2/RS", "/p1q2/RS/TT/UU"], ["/p1q2/RS"]},
52+
{["/user/%{U_123}", "/user/%{U_123}/orders", "/user/%{U_456}"], ["/user/%{U_123}"]},
53+
{["/calc/%{ModeA}/run", "/calc/%{ModeB}/run"], ["/calc/%{ModeA}/run"]},
54+
{["/calc/%{ModeC}/run/now", "/calc/%{ModeC}/run"], ["/calc/%{ModeC}/run/now"]},
55+
{["/A1b2C3", "/A1b2C3/D4e5"], ["/A1b2C3"]},
56+
{["/AABB", "/AABBCC"], ["/AABB", "/AABBCC"]},
57+
{["/srv/%{EnvX}/cfg", "/srv/%{EnvX}/cfg/edit"], ["/srv/%{EnvX}/cfg"]},
58+
{["/srv/%{Env_1}/cfg", "/srv/%{Env_2}/cfg"], ["/srv/%{Env_1}/cfg"]},
59+
{["/%{ROOT_X}", "/%{ROOT_Y}", "/%{ROOT_Y}/x"], ["/%{ROOT_X}"]},
60+
{["/root/%{Id1}/x", "/root/%{Id2}/x/y"], ["/root/%{Id1}/x"]},
61+
{["/root/%{Abc}/x/y", "/root/%{Abc}/x"], ["/root/%{Abc}/x/y"]},
62+
{["/kLm/Nop", "/kLm/Nop/Qrs", "/kLm/Tuv"], ["/kLm/Nop", "/kLm/Tuv"]},
63+
{["/data/%{KeyA}/val", "/data/%{KeyB}/val"], ["/data/%{KeyA}/val"]},
64+
{["/data/%{KeyA}/val", "/data/%{KeyA}/val/x"], ["/data/%{KeyA}/val"]},
65+
{["/zZz/%{Pid}/q", "/zZz/%{Pid}/q/r"], ["/zZz/%{Pid}/q"]},
66+
{["/zZz/%{Pid1}/q", "/zZz/%{Pid2}/w"], ["/zZz/%{Pid1}/q", "/zZz/%{Pid2}/w"]},
67+
{["/Mix/Aa", "/Mix/Aa/Bb", "/Mix/Cc"], ["/Mix/Aa", "/Mix/Cc"]},
68+
{["/auth/Login", "/auth/Login/Step2"], ["/auth/Login"]},
69+
{["/auth/%{FlowA}/step/next", "/auth/%{FlowA}/step"], ["/auth/%{FlowA}/step/next"]},
70+
{["/auth/%{FlowA}/step", "/auth/%{FlowB}"], ["/auth/%{FlowA}/step"]},
71+
{["/r1/r2/r3", "/r1/r2"], ["/r1/r2/r3"]},
72+
{["/r1/%{VarA}", "/r1/%{VarA}/x", "/r2"], ["/r1/%{VarA}", "/r2"]},
73+
{["/A_/B_", "/A_/B_/C_"], ["/A_/B_"]},
74+
{["/A_/B_/C_", "/A_/B_/C_/D_"], ["/A_/B_/C_"]},
75+
{["/node/%{N1}/leaf/%{L2}", "/node/%{N1}/leaf"], ["/node/%{N1}/leaf/%{L2}"]},
76+
{["/node/%{N1}/leaf/%{L2}", "/node/%{N2}/leaf/%{L3}"], ["/node/%{N1}/leaf/%{L2}"]},
77+
{["/Sx/Tx", "/Sx/Tx/Ux", "/Sx/Vx"], ["/Sx/Tx", "/Sx/Vx"]},
78+
{["/G1/H2", "/G1/H2/I3/J4"], ["/G1/H2"]},
79+
{["/lib/%{Pkg}/v", "/lib/%{Pkg}/v/1"], ["/lib/%{Pkg}/v"]},
80+
{["/lib/%{PkgA}/v", "/lib/%{PkgB}/v"], ["/lib/%{PkgA}/v"]},
81+
{["/cat/%{C1}/dog/%{D2}", "/cat/%{C1}/dog/%{D2}/x"], ["/cat/%{C1}/dog/%{D2}"]},
82+
{["/alpha/%{A1}/beta", "/alpha/%{A1}/beta/gamma"], ["/alpha/%{A1}/beta"]},
83+
{["/alpha/%{A1}/beta", "/alpha/%{A2}"], ["/alpha/%{A1}/beta"]},
84+
{["/xLong/SegName", "/xLong/SegName/Next"], ["/xLong/SegName"]},
85+
{["/Ping", "/Ping/Pong", "/Pang"], ["/Ping", "/Pang"]}
86+
]
87+
3288
@doc false
3389
describe "interface generator" do
3490
@describetag :success
@@ -51,6 +107,35 @@ defmodule Astarte.Core.Generators.InterfaceTest do
51107
end
52108
end
53109

110+
test "validate endpoints in :individual" do
111+
for {endpoints, results} <- @endpoint_cases do
112+
mappings =
113+
for endpoint <- endpoints do
114+
%Mapping{endpoint: endpoint}
115+
end
116+
117+
uniq_mappings = InterfaceGenerator.uniq_endpoints(mappings)
118+
119+
uniq_endpoints =
120+
for %Mapping{endpoint: endpoint} <- uniq_mappings do
121+
endpoint
122+
end
123+
124+
assert MapSet.new(results) == MapSet.new(uniq_endpoints)
125+
end
126+
end
127+
128+
property "validate endpoints in aggregation :object must be the same" do
129+
check all %Interface{mappings: mappings} <-
130+
InterfaceGenerator.interface(aggregation: :object),
131+
endpoints =
132+
mappings
133+
|> Enum.map(fn %Mapping{endpoint: endpoint} -> endpoint end)
134+
|> Enum.map(&Regex.replace(~r"/[^/]+$", &1, "")) do
135+
assert 1 == endpoints |> Enum.uniq() |> length()
136+
end
137+
end
138+
54139
@tag issue: 45
55140
property "custom interface creation" do
56141
gen_interface_changes =

0 commit comments

Comments
 (0)