001/*
002 * Copyright 2022-2026 Revetware LLC.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.soklet;
018
019import org.jspecify.annotations.NonNull;
020import org.jspecify.annotations.Nullable;
021
022import javax.annotation.concurrent.NotThreadSafe;
023import javax.annotation.concurrent.ThreadSafe;
024import java.util.Optional;
025
026import static com.soklet.McpContentValueSupport.annotationsEqual;
027import static com.soklet.McpContentValueSupport.annotationsHashCode;
028import static com.soklet.McpContentValueSupport.resourceContentsEqual;
029import static com.soklet.McpContentValueSupport.resourceContentsHashCode;
030import static com.soklet.internal.mcp.protocol.McpApplicationMetadata.requireApplicationMetadata;
031import static java.util.Objects.requireNonNull;
032
033/**
034 * Immutable embedded-resource MCP content block.
035 *
036 * @author <a href="https://www.revetkn.com">Mark Allen</a>
037 */
038@ThreadSafe
039public final class McpEmbeddedResource implements McpContentBlock {
040        @NonNull
041        private final McpResourceContents resource;
042        @Nullable
043        private final McpContentAnnotations annotations;
044        @NonNull
045        private final McpJsonObject metadata;
046
047        /**
048         * Vends a builder primed with resource contents.
049         *
050         * @param resourceContents embedded resource contents
051         * @return embedded-resource builder
052         */
053        @NonNull
054        public static Builder withResource(@NonNull McpResourceContents resourceContents) {
055                return new Builder(resourceContents);
056        }
057
058        private McpEmbeddedResource(@NonNull Builder builder) {
059                this.resource = builder.resource;
060                this.annotations = builder.annotations;
061                this.metadata = requireApplicationMetadata(builder.metadata);
062        }
063
064        /** @return embedded resource contents */
065        @NonNull
066        public McpResourceContents getResource() {
067                return this.resource;
068        }
069
070        /** @return content annotations, if supplied */
071        @Override
072        @NonNull
073        public Optional<@NonNull McpContentAnnotations> getAnnotations() {
074                return Optional.ofNullable(this.annotations);
075        }
076
077        /** @return immutable extension metadata */
078        @Override
079        @NonNull
080        public McpJsonObject getMetadata() {
081                return this.metadata;
082        }
083
084        /** @return whether every content property is structurally equal */
085        @Override
086        public boolean equals(@Nullable Object other) {
087                if (this == other)
088                        return true;
089                if (!(other instanceof McpEmbeddedResource content))
090                        return false;
091                return resourceContentsEqual(this.resource, content.resource)
092                                && annotationsEqual(this.annotations, content.annotations)
093                                && this.metadata.equals(content.metadata);
094        }
095
096        /** @return structural content hash code */
097        @Override
098        public int hashCode() {
099                int result = resourceContentsHashCode(this.resource);
100                result = 31 * result + annotationsHashCode(this.annotations);
101                result = 31 * result + this.metadata.hashCode();
102                return result;
103        }
104
105        /**
106         * Mutable builder for immutable {@link McpEmbeddedResource}.
107         *
108         * @author <a href="https://www.revetkn.com">Mark Allen</a>
109         */
110        @NotThreadSafe
111        public static final class Builder {
112                @NonNull
113                private final McpResourceContents resource;
114                @Nullable
115                private McpContentAnnotations annotations;
116                @NonNull
117                private McpJsonObject metadata = McpJsonObject.emptyInstance();
118
119                private Builder(@NonNull McpResourceContents resource) {
120                        this.resource = requireNonNull(resource);
121                }
122
123                /** @param annotations content annotations
124                 * @return this builder */
125                @NonNull
126                public Builder annotations(@NonNull McpContentAnnotations annotations) {
127                        this.annotations = requireNonNull(annotations);
128                        return this;
129                }
130
131                /** @param metadata protocol extension metadata
132                 * @return this builder */
133                @NonNull
134                public Builder metadata(@NonNull McpJsonObject metadata) {
135                        this.metadata = requireNonNull(metadata);
136                        return this;
137                }
138
139                /**
140                 * @return immutable embedded-resource content
141                 * @throws IllegalArgumentException if the contents carry Apps resource
142                 * metadata, which is valid only on resource reads
143                 */
144                @NonNull
145                public McpEmbeddedResource build() {
146                        if (McpAppMetadataSupport.effectiveResourceMetadata(this.resource.getMetadata(),
147                                        this.resource.getAppResourceMetadata().orElse(null)).isPresent())
148                                throw new IllegalArgumentException(
149                                                "MCP Apps resource metadata is not valid on embedded resource contents.");
150                        return new McpEmbeddedResource(this);
151                }
152        }
153}