diff --git a/src/main/java/io/naftiko/Capability.java b/src/main/java/io/naftiko/Capability.java index e62afeb..2d8f2a8 100644 --- a/src/main/java/io/naftiko/Capability.java +++ b/src/main/java/io/naftiko/Capability.java @@ -14,11 +14,15 @@ package io.naftiko; import java.io.File; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import io.naftiko.engine.ExternalRefResolver; +import io.naftiko.spec.ExecutionContext; import io.naftiko.engine.consumes.ClientAdapter; import io.naftiko.engine.consumes.HttpClientAdapter; import io.naftiko.engine.exposes.ApiServerAdapter; @@ -39,10 +43,36 @@ public class Capability { private volatile NaftikoSpec spec; private volatile List serverAdapters; private volatile List clientAdapters; + private volatile Map externalRefVariables; - public Capability(NaftikoSpec spec) { + public Capability(NaftikoSpec spec) throws Exception { + this(spec, null); + } + + /** + * Creates a capability with an optional capability directory for external ref file resolution. + * + * @param spec The Naftiko specification + * @param capabilityDir Directory containing the capability file (null for default) + * @throws Exception if external refs cannot be resolved + */ + public Capability(NaftikoSpec spec, String capabilityDir) throws Exception { this.spec = spec; + // Resolve external references early for injection into adapters + ExternalRefResolver refResolver = new ExternalRefResolver(); + ExecutionContext context = new ExecutionContext() { + @Override + public String getVariable(String key) { + return System.getenv(key); + } + }; + Map resolvedRefs = refResolver.resolve( + spec.getExternalRefs(), + context); + // Convert Map to Map for compatibility + this.externalRefVariables = new HashMap<>(resolvedRefs); + // Initialize client adapters first this.clientAdapters = new CopyOnWriteArrayList<>(); @@ -84,6 +114,16 @@ public List getServerAdapters() { return serverAdapters; } + /** + * Returns the map of resolved external reference variables. + * These are injected into parameter resolution contexts. + * + * @return Map of variable name to resolved value + */ + public Map getExternalRefVariables() { + return externalRefVariables; + } + public void start() throws Exception { for (ClientAdapter adapter : getClientAdapters()) { adapter.start(); @@ -124,7 +164,9 @@ public static void main(String[] args) { // Ignore unknown properties to handle potential Restlet framework classes mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); NaftikoSpec spec = mapper.readValue(file, NaftikoSpec.class); - Capability capability = new Capability(spec); + // Pass the capability directory for external ref file resolution + String capabilityDir = file.getParent(); + Capability capability = new Capability(spec, capabilityDir); capability.start(); System.out.println("Capability started successfully."); } catch (Exception e) { diff --git a/src/main/java/io/naftiko/engine/ExternalRefResolver.java b/src/main/java/io/naftiko/engine/ExternalRefResolver.java new file mode 100644 index 0000000..0119c8e --- /dev/null +++ b/src/main/java/io/naftiko/engine/ExternalRefResolver.java @@ -0,0 +1,188 @@ +/** + * Copyright 2025-2026 Naftiko + * + * 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. + */ +package io.naftiko.engine; + +import java.io.IOException; +import java.lang.Iterable; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import io.naftiko.spec.ExecutionContext; +import io.naftiko.spec.ExternalRefKeysSpec; +import io.naftiko.spec.ExternalRefSpec; +import io.naftiko.spec.FileResolvedExternalRefSpec; +import io.naftiko.spec.RuntimeResolvedExternalRefSpec; + +/** + * Resolver for external references that supports both file-based and runtime-based injection. + * File-based refs load configuration from JSON/YAML files. + * Runtime-based refs extract values from an ExecutionContext (environment variables, secrets, etc.). + */ +public class ExternalRefResolver { + + private final ObjectMapper yamlMapper; + private final ObjectMapper jsonMapper; + + public ExternalRefResolver() { + this.yamlMapper = new ObjectMapper(new YAMLFactory()); + this.jsonMapper = new ObjectMapper(); + } + + /** + * Resolves all external references and returns a map of resolved variables. + * + * @param externalRefs List of external reference specifications to resolve + * @param context Execution context for runtime variable resolution + * @return Map of variable name to resolved value + * @throws IOException if file-based ref cannot be read + */ + public Map resolve(Iterable externalRefs, ExecutionContext context) + throws IOException { + Map resolved = new HashMap<>(); + + if (externalRefs == null) { + return resolved; + } + + for (ExternalRefSpec ref : externalRefs) { + if (ref instanceof FileResolvedExternalRefSpec) { + Map fileVars = resolveFileReference((FileResolvedExternalRefSpec) ref); + resolved.putAll(fileVars); + } else if (ref instanceof RuntimeResolvedExternalRefSpec) { + Map runtimeVars = resolveRuntimeReference((RuntimeResolvedExternalRefSpec) ref, + context); + resolved.putAll(runtimeVars); + } + } + + return resolved; + } + + /** + * Resolves a file-based external reference by reading the specified file and extracting variables + * according to the keys mapping. Supports JSON and YAML formats. + * + * @param ref The file-resolved external reference specification + * @return Map of variable name to resolved value + * @throws IOException if file cannot be read or key is missing + */ + public Map resolveFileReference(FileResolvedExternalRefSpec ref) throws IOException { + if (ref.getUri() == null || ref.getUri().isEmpty()) { + throw new IOException("Invalid ExternalRef: missing uri"); + } + + ExternalRefKeysSpec keysSpec = ref.getKeys(); + if (keysSpec == null || keysSpec.getKeys() == null || keysSpec.getKeys().isEmpty()) { + throw new IOException("Invalid ExternalRef: missing keys"); + } + + // Parse file content + Map fileContent = parseFileContent(ref.getUri()); + + // Extract variables using the key mappings + Map resolved = new HashMap<>(); + for (Map.Entry mapping : keysSpec.getKeys().entrySet()) { + String variableName = mapping.getKey(); // e.g., "notion_token" + String fileKey = mapping.getValue(); // e.g., "NOTION_TOKEN" + + Object value = fileContent.get(fileKey); + if (value == null) { + throw new IOException("Invalid ExternalRef: key '" + fileKey + "' not found in file"); + } + + resolved.put(variableName, String.valueOf(value)); + } + + return resolved; + } + + /** + * Resolves a runtime-based external reference by extracting variables from an ExecutionContext. + * + * @param ref The runtime-resolved external reference specification + * @param context The execution context providing variable values + * @return Map of variable name to resolved value + * @throws IOException if variable is missing + */ + public Map resolveRuntimeReference(RuntimeResolvedExternalRefSpec ref, + ExecutionContext context) throws IOException { + ExternalRefKeysSpec keysSpec = ref.getKeys(); + if (keysSpec == null || keysSpec.getKeys() == null || keysSpec.getKeys().isEmpty()) { + throw new IOException("Invalid ExternalRef: missing keys"); + } + + Map resolved = new HashMap<>(); + for (Map.Entry mapping : keysSpec.getKeys().entrySet()) { + String variableName = mapping.getKey(); // e.g., "api_key" + String contextKey = mapping.getValue(); // e.g., "API_KEY" + + String value = context.getVariable(contextKey); + if (value == null) { + throw new IOException("Invalid ExternalRef: context variable '" + contextKey + + "' not found"); + } + + resolved.put(variableName, value); + } + + return resolved; + } + + /** + * Parses file content (JSON or YAML) and returns a flat map of key-value pairs. + * + * @param uriString The file URI (file:// format) + * @return Map of keys to values + * @throws IOException if file cannot be read or parsed + */ + @SuppressWarnings("unchecked") + private Map parseFileContent(String uriString) throws IOException { + try { + URI uri = URI.create(uriString); + Path filePath = Paths.get(uri.getPath()); + + if (!Files.exists(filePath)) { + throw new IOException("File not found: " + filePath); + } + + String content = Files.readString(filePath); + + // Detect format by file extension + if (uriString.endsWith(".yaml") || uriString.endsWith(".yml")) { + Object parsed = yamlMapper.readValue(content, Object.class); + if (parsed instanceof Map) { + return (Map) parsed; + } + throw new IOException("YAML file does not contain a map at root level"); + } else { // JSON by default + Object parsed = jsonMapper.readValue(content, Object.class); + if (parsed instanceof Map) { + return (Map) parsed; + } + throw new IOException("JSON file does not contain an object at root level"); + } + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw new IOException("Failed to parse external ref file: " + e.getMessage(), e); + } + } + +} diff --git a/src/main/java/io/naftiko/spec/ExecutionContext.java b/src/main/java/io/naftiko/spec/ExecutionContext.java new file mode 100644 index 0000000..00ae3f1 --- /dev/null +++ b/src/main/java/io/naftiko/spec/ExecutionContext.java @@ -0,0 +1,31 @@ +/** + * Copyright 2025-2026 Naftiko + * + * 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. + */ +package io.naftiko.spec; + +/** + * Provides runtime variables for external reference resolution. + * Implementations can source variables from environment variables, secrets managers, + * or other context stores. + */ +public interface ExecutionContext { + + /** + * Retrieves a variable value from the execution context. + * + * @param key The variable key to look up + * @return The variable value, or null if not found + */ + String getVariable(String key); + +} diff --git a/src/main/java/io/naftiko/spec/ExternalRefKeysSpec.java b/src/main/java/io/naftiko/spec/ExternalRefKeysSpec.java new file mode 100644 index 0000000..438064a --- /dev/null +++ b/src/main/java/io/naftiko/spec/ExternalRefKeysSpec.java @@ -0,0 +1,63 @@ +/** + * Copyright 2025-2026 Naftiko + * + * 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. + */ +package io.naftiko.spec; + +import java.util.HashMap; +import java.util.Map; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * External Reference Keys Specification Element. + * Maps variable names to keys in the resolved file content or runtime context. + * Uses @JsonAnySetter to allow arbitrary key-value pairs in the YAML/JSON. + */ +public class ExternalRefKeysSpec { + + private Map keys; + + public ExternalRefKeysSpec() { + this.keys = new HashMap<>(); + } + + public ExternalRefKeysSpec(Map keys) { + this.keys = keys != null ? new HashMap<>(keys) : new HashMap<>(); + } + + @JsonAnySetter + public void setKey(String variableName, Object value) { + if (value != null) { + this.keys.put(variableName, value.toString()); + } + } + + @JsonValue + public Map getKeys() { + return keys; + } + + public void setKeys(Map keys) { + this.keys = keys != null ? new HashMap<>(keys) : new HashMap<>(); + } + + public void putKey(String variableName, String contextKey) { + this.keys.put(variableName, contextKey); + } + + public String getKey(String variableName) { + return this.keys.get(variableName); + } + +} + diff --git a/src/main/java/io/naftiko/spec/ExternalRefSpec.java b/src/main/java/io/naftiko/spec/ExternalRefSpec.java new file mode 100644 index 0000000..f623afe --- /dev/null +++ b/src/main/java/io/naftiko/spec/ExternalRefSpec.java @@ -0,0 +1,85 @@ +/** + * Copyright 2025-2026 Naftiko + * + * 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. + */ +package io.naftiko.spec; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +/** + * Base External Reference Specification Element. + * Supports both file-resolved and runtime-resolved references for variable injection. + */ +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = FileResolvedExternalRefSpec.class, name = "environment"), + @JsonSubTypes.Type(value = RuntimeResolvedExternalRefSpec.class, name = "variables") +}) +public abstract class ExternalRefSpec { + + protected volatile String name; + + protected volatile String type; + + protected volatile String resolution; + + protected volatile ExternalRefKeysSpec keys; + + public ExternalRefSpec() { + this(null, null, null, null); + } + + public ExternalRefSpec(String name, String type, String resolution, ExternalRefKeysSpec keys) { + this.name = name; + this.type = type; + this.resolution = resolution; + this.keys = keys; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getResolution() { + return resolution; + } + + public void setResolution(String resolution) { + this.resolution = resolution; + } + + public ExternalRefKeysSpec getKeys() { + return keys; + } + + public void setKeys(ExternalRefKeysSpec keys) { + this.keys = keys; + } + +} diff --git a/src/main/java/io/naftiko/spec/FileResolvedExternalRefSpec.java b/src/main/java/io/naftiko/spec/FileResolvedExternalRefSpec.java new file mode 100644 index 0000000..e871dc5 --- /dev/null +++ b/src/main/java/io/naftiko/spec/FileResolvedExternalRefSpec.java @@ -0,0 +1,52 @@ +/** + * Copyright 2025-2026 Naftiko + * + * 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. + */ +package io.naftiko.spec; + +/** + * File-Resolved External Reference Specification Element. + * Loads variables from a local file. Intended for local development only. + */ +public class FileResolvedExternalRefSpec extends ExternalRefSpec { + + private volatile String description; + + private volatile String uri; + + public FileResolvedExternalRefSpec() { + super("environment-file", "environment", "file", null); + } + + public FileResolvedExternalRefSpec(String name, String description, String uri, ExternalRefKeysSpec keys) { + super(name, "environment", "file", keys); + this.description = description; + this.uri = uri; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } + +} diff --git a/src/main/java/io/naftiko/spec/NaftikoSpec.java b/src/main/java/io/naftiko/spec/NaftikoSpec.java index 7baa598..27f6c46 100644 --- a/src/main/java/io/naftiko/spec/NaftikoSpec.java +++ b/src/main/java/io/naftiko/spec/NaftikoSpec.java @@ -13,6 +13,10 @@ */ package io.naftiko.spec; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import com.fasterxml.jackson.annotation.JsonInclude; + /** * Naftiko Specification Root, including version and capabilities */ @@ -22,11 +26,15 @@ public class NaftikoSpec { private volatile InfoSpec info; + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private final List externalRefs; + private volatile CapabilitySpec capability; public NaftikoSpec(String naftiko, InfoSpec info, CapabilitySpec capability) { this.naftiko = naftiko; this.info = info; + this.externalRefs = new CopyOnWriteArrayList<>(); this.capability = capability; } @@ -50,12 +58,16 @@ public void setInfo(InfoSpec info) { this.info = info; } + public List getExternalRefs() { + return externalRefs; + } + public CapabilitySpec getCapability() { return capability; } public void setCapability(CapabilitySpec capability) { this.capability = capability; - } + } } diff --git a/src/main/java/io/naftiko/spec/RuntimeResolvedExternalRefSpec.java b/src/main/java/io/naftiko/spec/RuntimeResolvedExternalRefSpec.java new file mode 100644 index 0000000..61cb588 --- /dev/null +++ b/src/main/java/io/naftiko/spec/RuntimeResolvedExternalRefSpec.java @@ -0,0 +1,32 @@ +/** + * Copyright 2025-2026 Naftiko + * + * 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. + */ +package io.naftiko.spec; + +/** + * Runtime-Resolved External Reference Specification Element. + * Variables are injected at runtime by the execution environment (default). + * The capability document does not specify where the values come from + * — this is delegated to the deployment platform. + */ +public class RuntimeResolvedExternalRefSpec extends ExternalRefSpec { + + public RuntimeResolvedExternalRefSpec() { + super("env-runtime", "variables", "runtime", null); + } + + public RuntimeResolvedExternalRefSpec(String name, ExternalRefKeysSpec keys) { + super(name, "variables", "runtime", keys); + } + +}