Skip to content

Commit 965c78e

Browse files
authored
Resource: add static validation for all the known resource types (#392)
1 parent 072b0ac commit 965c78e

File tree

7 files changed

+795
-84
lines changed

7 files changed

+795
-84
lines changed

langstream-api/src/main/java/ai/langstream/api/util/ConfigurationUtils.java

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.List;
2222
import java.util.Map;
2323
import java.util.Set;
24+
import java.util.function.Supplier;
2425
import java.util.stream.Collectors;
2526

2627
/**
@@ -155,4 +156,100 @@ public static Map<String, Object> getMap(
155156
+ ", expecting a Map, got got a "
156157
+ value.getClass().getName());
157158
}
159+
160+
public static <T> T requiredField(
161+
Map<String, Object> configuration, String name, Supplier<String> definition) {
162+
Object value = configuration.get(name);
163+
if (value == null) {
164+
throw new IllegalArgumentException(
165+
"Missing required field '" + name + "' in " + definition.get());
166+
}
167+
return (T) value;
168+
}
169+
170+
public static String requiredNonEmptyField(
171+
Map<String, Object> configuration, String name, Supplier<String> definition) {
172+
Object value = configuration.get(name);
173+
if (value == null || value.toString().isEmpty()) {
174+
throw new IllegalArgumentException(
175+
"Missing required field '" + name + "' in " + definition.get());
176+
}
177+
return value.toString();
178+
}
179+
180+
public static void validateEnumField(
181+
Map<String, Object> configuration,
182+
String name,
183+
Set<String> values,
184+
Supplier<String> definition) {
185+
Object value = configuration.get(name);
186+
if (value == null || value.toString().isEmpty()) {
187+
return;
188+
}
189+
if (!values.contains(value.toString())) {
190+
throw new IllegalArgumentException(
191+
"Value "
192+
+ value
193+
+ " is not allowed for field '"
194+
+ name
195+
+ ", only "
196+
+ values
197+
+ " are allowed', in "
198+
+ definition.get());
199+
}
200+
}
201+
202+
public static void validateInteger(
203+
Map<String, Object> configuration,
204+
String name,
205+
int min,
206+
int max,
207+
Supplier<String> definition) {
208+
Object value = configuration.get(name);
209+
if (value == null || value.toString().isEmpty()) {
210+
return;
211+
}
212+
int number;
213+
if (value instanceof Number) {
214+
number = ((Number) value).intValue();
215+
} else {
216+
number = Integer.parseInt(value.toString());
217+
}
218+
if (number < min) {
219+
throw new IllegalArgumentException(
220+
"Integer field '"
221+
+ name
222+
+ "' is "
223+
+ number
224+
+ ", but it must be greater or equals to "
225+
+ min
226+
+ ", in "
227+
+ definition.get());
228+
}
229+
if (number > max) {
230+
throw new IllegalArgumentException(
231+
"Integer field '"
232+
+ name
233+
+ "' is "
234+
+ number
235+
+ ", but it must be less or equals to "
236+
+ max
237+
+ ", in "
238+
+ definition.get());
239+
}
240+
}
241+
242+
public static void requiredListField(
243+
Map<String, Object> configuration, String name, Supplier<String> definition) {
244+
Object value = configuration.get(name);
245+
if (value == null) {
246+
throw new IllegalArgumentException(
247+
"Missing required field '" + name + "' in " + definition.get());
248+
}
249+
250+
if (!(value instanceof List)) {
251+
throw new IllegalArgumentException(
252+
"Expecting a list in the field '" + name + "' in " + definition.get());
253+
}
254+
}
158255
}

langstream-core/src/main/java/ai/langstream/impl/assets/CassandraAssetsProvider.java

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
*/
1616
package ai.langstream.impl.assets;
1717

18+
import static ai.langstream.api.util.ConfigurationUtils.requiredField;
19+
import static ai.langstream.api.util.ConfigurationUtils.requiredListField;
20+
import static ai.langstream.api.util.ConfigurationUtils.requiredNonEmptyField;
21+
1822
import ai.langstream.api.model.AssetDefinition;
1923
import ai.langstream.api.util.ConfigurationUtils;
2024
import ai.langstream.impl.common.AbstractAssetProvider;
@@ -32,36 +36,37 @@ public CassandraAssetsProvider() {
3236
@Override
3337
protected void validateAsset(AssetDefinition assetDefinition, Map<String, Object> asset) {
3438
Map<String, Object> configuration = ConfigurationUtils.getMap("config", null, asset);
35-
requiredField(assetDefinition, configuration, "datasource");
39+
requiredField(configuration, "datasource", describe(assetDefinition));
3640
final Map<String, Object> datasource =
3741
ConfigurationUtils.getMap("datasource", Map.of(), configuration);
3842
final Map<String, Object> datasourceConfiguration =
3943
ConfigurationUtils.getMap("configuration", Map.of(), datasource);
4044
switch (assetDefinition.getAssetType()) {
4145
case "cassandra-table" -> {
42-
requiredNonEmptyField(assetDefinition, configuration, "table-name");
43-
requiredNonEmptyField(assetDefinition, configuration, "keyspace");
44-
requiredListField(assetDefinition, configuration, "create-statements");
46+
requiredNonEmptyField(configuration, "table-name", describe(assetDefinition));
47+
requiredNonEmptyField(configuration, "keyspace", describe(assetDefinition));
48+
requiredListField(configuration, "create-statements", describe(assetDefinition));
4549
}
4650
case "cassandra-keyspace" -> {
47-
requiredNonEmptyField(assetDefinition, configuration, "keyspace");
48-
requiredListField(assetDefinition, configuration, "create-statements");
51+
requiredNonEmptyField(configuration, "keyspace", describe(assetDefinition));
52+
requiredListField(configuration, "create-statements", describe(assetDefinition));
4953
if (datasourceConfiguration.containsKey("secureBundle")
5054
|| datasourceConfiguration.containsKey("database")) {
5155
throw new IllegalArgumentException("Use astra-keyspace for AstraDB services");
5256
}
5357
}
5458
case "astra-keyspace" -> {
55-
requiredNonEmptyField(assetDefinition, configuration, "keyspace");
59+
requiredNonEmptyField(configuration, "keyspace", describe(assetDefinition));
5660
if (!datasourceConfiguration.containsKey("secureBundle")
5761
&& !datasourceConfiguration.containsKey("database")) {
5862
throw new IllegalArgumentException(
5963
"Use cassandra-keyspace for a standard Cassandra service (not AstraDB)");
6064
}
6165
// are we are using the AstraDB SDK we need also the AstraCS token and
6266
// the name of the database
63-
requiredNonEmptyField(assetDefinition, datasourceConfiguration, "token");
64-
requiredNonEmptyField(assetDefinition, datasourceConfiguration, "database");
67+
requiredNonEmptyField(datasourceConfiguration, "token", describe(assetDefinition));
68+
requiredNonEmptyField(
69+
datasourceConfiguration, "database", describe(assetDefinition));
6570
}
6671
default -> throw new IllegalStateException(
6772
"Unexpected value: " + assetDefinition.getAssetType());

langstream-core/src/main/java/ai/langstream/impl/common/AbstractAssetProvider.java

Lines changed: 13 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package ai.langstream.impl.common;
1717

18+
import static ai.langstream.api.util.ConfigurationUtils.requiredNonEmptyField;
19+
1820
import ai.langstream.api.model.Application;
1921
import ai.langstream.api.model.AssetDefinition;
2022
import ai.langstream.api.model.Module;
@@ -25,9 +27,9 @@
2527
import ai.langstream.api.runtime.ExecutionPlan;
2628
import ai.langstream.api.runtime.PluginsRegistry;
2729
import java.util.HashMap;
28-
import java.util.List;
2930
import java.util.Map;
3031
import java.util.Set;
32+
import java.util.function.Supplier;
3133

3234
/** Utility method to implement an AssetNodeProvider. */
3335
public abstract class AbstractAssetProvider implements AssetNodeProvider {
@@ -84,9 +86,9 @@ private Map<String, Object> planAsset(
8486
if (lookupResource(key)) {
8587
String resourceId =
8688
requiredNonEmptyField(
87-
assetDefinition,
8889
assetDefinition.getConfig(),
89-
key);
90+
key,
91+
describe(assetDefinition));
9092
Resource resource = resources.get(resourceId);
9193
if (resource != null) {
9294
Map<String, Object> resourceImplementation =
@@ -117,67 +119,13 @@ public boolean supports(String type, ComputeClusterRuntime clusterRuntime) {
117119
return supportedType.contains(type);
118120
}
119121

120-
protected static <T> T requiredField(
121-
AssetDefinition assetDefinition, Map<String, Object> configuration, String name) {
122-
Object value = configuration.get(name);
123-
if (value == null) {
124-
throw new IllegalArgumentException(
125-
"Missing required field '"
126-
+ name
127-
+ "' in assert definition, type="
128-
+ assetDefinition.getAssetType()
129-
+ ", name="
130-
+ assetDefinition.getName()
131-
+ ", id="
132-
+ assetDefinition.getId());
133-
}
134-
return (T) value;
135-
}
136-
137-
protected static String requiredNonEmptyField(
138-
AssetDefinition assetDefinition, Map<String, Object> configuration, String name) {
139-
Object value = configuration.get(name);
140-
if (value == null || value.toString().isEmpty()) {
141-
throw new IllegalArgumentException(
142-
"Missing required field '"
143-
+ name
144-
+ "' in assert definition, type="
145-
+ assetDefinition.getAssetType()
146-
+ ", name="
147-
+ assetDefinition.getName()
148-
+ ", id="
149-
+ assetDefinition.getId());
150-
}
151-
return value.toString();
152-
}
153-
154-
protected static void requiredListField(
155-
AssetDefinition assetDefinition, Map<String, Object> configuration, String name) {
156-
Object value = configuration.get(name);
157-
if (value == null) {
158-
throw new IllegalArgumentException(
159-
"Missing required field '"
160-
+ name
161-
+ "' in assert definition, type="
162-
+ assetDefinition.getAssetType()
163-
+ ", name="
164-
+ assetDefinition.getName()
165-
+ ", id="
166-
+ assetDefinition.getId());
167-
}
168-
169-
if (!(value instanceof List)) {
170-
throw new IllegalArgumentException(
171-
"Expecting a list in the field '"
172-
+ name
173-
+ "' in assert definition, type="
174-
+ assetDefinition.getAssetType()
175-
+ ", name="
176-
+ assetDefinition.getName()
177-
+ ", id="
178-
+ assetDefinition.getId()
179-
+ " but got a "
180-
+ value.getClass().getName());
181-
}
122+
protected static Supplier<String> describe(AssetDefinition assetDefinition) {
123+
return () ->
124+
" assert definition, type="
125+
+ assetDefinition.getAssetType()
126+
+ ", name="
127+
+ assetDefinition.getName()
128+
+ ", id="
129+
+ assetDefinition.getId();
182130
}
183131
}

langstream-core/src/main/java/ai/langstream/impl/resources/AIProvidersResourceProvider.java

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,22 @@
1515
*/
1616
package ai.langstream.impl.resources;
1717

18+
import static ai.langstream.api.util.ConfigurationUtils.getMap;
19+
import static ai.langstream.api.util.ConfigurationUtils.getString;
20+
import static ai.langstream.api.util.ConfigurationUtils.requiredField;
21+
import static ai.langstream.api.util.ConfigurationUtils.requiredNonEmptyField;
22+
import static ai.langstream.api.util.ConfigurationUtils.validateEnumField;
23+
1824
import ai.langstream.api.model.Module;
1925
import ai.langstream.api.model.Resource;
2026
import ai.langstream.api.runtime.ComputeClusterRuntime;
2127
import ai.langstream.api.runtime.ExecutionPlan;
2228
import ai.langstream.api.runtime.PluginsRegistry;
2329
import ai.langstream.api.runtime.ResourceNodeProvider;
30+
import com.fasterxml.jackson.databind.ObjectMapper;
2431
import java.util.Map;
2532
import java.util.Set;
33+
import java.util.function.Supplier;
2634

2735
public class AIProvidersResourceProvider implements ResourceNodeProvider {
2836

@@ -36,12 +44,72 @@ public Map<String, Object> createImplementation(
3644
ExecutionPlan executionPlan,
3745
ComputeClusterRuntime clusterRuntime,
3846
PluginsRegistry pluginsRegistry) {
39-
Map<String, Object> configuration = resource.configuration();
47+
switch (resource.type()) {
48+
case "open-ai-configuration" -> {
49+
validateOpenAIConfigurationResource(resource);
50+
}
51+
case "hugging-face-configuration" -> {
52+
validateHuggingFaceConfigurationResource(resource);
53+
}
54+
case "vertex-configuration" -> {
55+
validateVertexConfigurationResource(resource);
56+
}
57+
default -> throw new IllegalStateException();
58+
}
4059
return resource.configuration();
4160
}
4261

62+
private void validateVertexConfigurationResource(Resource resource) {
63+
Map<String, Object> configuration = resource.configuration();
64+
requiredNonEmptyField(configuration, "url", describe(resource));
65+
requiredNonEmptyField(configuration, "region", describe(resource));
66+
requiredNonEmptyField(configuration, "project", describe(resource));
67+
68+
String token = getString("token", "", configuration);
69+
String serviceAccountJson = getString("serviceAccountJson", "", configuration);
70+
if (!token.isEmpty() && !serviceAccountJson.isEmpty()) {
71+
throw new IllegalArgumentException(
72+
"Only one of token and serviceAccountJson should be provided in "
73+
+ describe(resource).get());
74+
}
75+
if (token.isEmpty()) {
76+
requiredNonEmptyField(configuration, "serviceAccountJson", describe(resource));
77+
}
78+
if (!serviceAccountJson.isEmpty()) {
79+
try {
80+
new ObjectMapper().readValue(serviceAccountJson, Map.class);
81+
} catch (Exception e) {
82+
throw new IllegalArgumentException(
83+
"Invalid JSON for field serviceAccountJson in " + describe(resource).get(),
84+
e);
85+
}
86+
}
87+
}
88+
89+
private void validateHuggingFaceConfigurationResource(Resource resource) {
90+
Map<String, Object> configuration = resource.configuration();
91+
validateEnumField(configuration, "provider", Set.of("local", "api"), describe(resource));
92+
requiredField(configuration, "model", describe(resource));
93+
getMap("options", Map.of(), configuration);
94+
getMap("arguments", Map.of(), configuration);
95+
}
96+
97+
private void validateOpenAIConfigurationResource(Resource resource) {
98+
Map<String, Object> configuration = resource.configuration();
99+
validateEnumField(configuration, "provider", Set.of("azure", "openai"), describe(resource));
100+
String provider = getString("provider", "openai", configuration);
101+
if (provider.equals("azure")) {
102+
requiredField(configuration, "url", describe(resource));
103+
}
104+
requiredField(configuration, "access-key", describe(resource));
105+
}
106+
43107
@Override
44108
public boolean supports(String type, ComputeClusterRuntime clusterRuntime) {
45109
return SUPPORTED_TYPES.contains(type);
46110
}
111+
112+
protected static Supplier<String> describe(Resource resource) {
113+
return () -> "resource with id = " + resource.id() + " of type " + resource.type();
114+
}
47115
}

0 commit comments

Comments
 (0)