001/*
002 * SPDX-License-Identifier: Apache-2.0
003 *
004 * Copyright 2023-2026 The Enola <https://enola.dev> Authors
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License");
007 * you may not use this file except in compliance with the License.
008 * You may obtain a copy of the License at
009 *
010 *     https://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package dev.enola.common.protobuf;
019
020import com.google.errorprone.annotations.CanIgnoreReturnValue;
021import com.google.errorprone.annotations.CheckReturnValue;
022import com.google.protobuf.Descriptors;
023import com.google.protobuf.MessageOrBuilder;
024
025import dev.enola.common.validation.Validation;
026import dev.enola.common.validation.Validations;
027
028import java.util.*;
029
030public class MessageValidators {
031    private final Map<Descriptors.Descriptor, List<MessageValidator<Object, MessageOrBuilder>>>
032            map = new HashMap<>();
033
034    @CanIgnoreReturnValue
035    public MessageValidators register(
036            MessageValidator<?, ?> validator, Descriptors.Descriptor descriptor) {
037        map.computeIfAbsent(descriptor, descriptor1 -> new ArrayList<>())
038                .add((MessageValidator<Object, MessageOrBuilder>) validator);
039        return this;
040    }
041
042    @CheckReturnValue
043    public Result validate(MessageOrBuilder message) {
044        var results = Result.newBuilder();
045        validate(null, message, results);
046        return results.build();
047    }
048
049    public void validate(MessageOrBuilder message, MessageValidators.Result.Builder r) {
050        validate(null, message, r);
051    }
052
053    public void validate(
054            Object context, MessageOrBuilder message, MessageValidators.Result.Builder results) {
055        var validators = map.get(message.getDescriptorForType());
056        if (validators == null) {
057            return;
058        }
059        for (var validator : validators) {
060            validator.validate(context, message, results);
061        }
062    }
063
064    @CheckReturnValue
065    public Result validate(Object context, MessageOrBuilder message) {
066        var results = Result.newBuilder();
067        validate(context, message, results);
068        return results.build();
069    }
070
071    public static class Result {
072        private final Validations proto;
073
074        private Result(Validations proto) {
075            this.proto = proto;
076        }
077
078        public static Builder newBuilder() {
079            return new Builder();
080        }
081
082        @CheckReturnValue
083        public Validations toMessage() {
084            return proto;
085        }
086
087        public void throwIt() throws ValidationException {
088            if (proto.getValidationsCount() > 0) {
089                throw new ValidationException(proto);
090            }
091        }
092
093        public static class Builder {
094            private final Validations.Builder result = Validations.newBuilder();
095
096            private Builder() {}
097
098            public Result build() {
099                var proto = result.build();
100                return new Result(proto);
101            }
102
103            public void add(Descriptors.GenericDescriptor descriptor, String error) {
104                // TODO Refine setPath() ... it should "aggregate" from "parents" ...
105                result.addValidations(
106                        Validation.newBuilder()
107                                .setPath(descriptor.getName())
108                                .setError(error)
109                                .build());
110            }
111        }
112    }
113}