package org.shedlang.compiler.config.json; import org.shedlang.compiler.config.errors.ConfigError; import org.shedlang.compiler.config.errors.Json5SyntaxConfigError; import org.shedlang.compiler.config.errors.UnexpectedTypeConfigError; import org.shedlang.compiler.sources.FileFragmentSource; import org.zwobble.json5.parser.Json5ParseError; import org.zwobble.json5.parser.Json5Parser; import org.zwobble.json5.values.*; import org.zwobble.sourcetext.SourceText; import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.OptionalLong; public class Json5ObjectWithSource { private final Json5Object object; private Json5ObjectWithSource(Json5Object object) { this.object = object; } public static Json5ObjectWithSource parse(SourceText sourceText) { try { var document = Json5Parser.parse(sourceText); if (document instanceof Json5Object object) { return new Json5ObjectWithSource(object); } else { // TODO: write a test for this. throw new UnexpectedTypeConfigError( document.path(), Json5Object.class, document.getClass(), new FileFragmentSource(document.sourceRange()) ); } } catch (Json5ParseError error) { throw Json5SyntaxConfigError.fromJson5ParseError(error); } } public Optional getObject(String memberName) { return getValueOfType(Json5Object.class, memberName) .map(object -> new Json5ObjectWithSource(object)); } private Optional getArray(String memberName) { return getValueOfType(Json5Array.class, memberName); } public Optional> getArrayOfObjects(String memberName) { return getArray(memberName).map(array -> { var objects = new ArrayList(); var index = 0; for (var element : array.elements()) { var object = castJson5Value( Json5Object.class, element ); objects.add(new Json5ObjectWithSource(object)); index++; } return objects; }); } public Optional getStringOrNone(String memberName) { return getValueOfType(Json5String.class, memberName); } public Json5String getString(String memberName) { return getStringOrNone(memberName) .orElseThrow(() -> new ConfigError( // TODO: escape member name MessageFormat.format("{0} is missing {1}", object.path(), memberName), new FileFragmentSource(object.sourceRange()) )); } public OptionalLong getLongOrNone(String memberName) { var numberFinite = getValueOfType(Json5NumberFinite.class, memberName); if (numberFinite.isEmpty()) { return OptionalLong.empty(); } try { return OptionalLong.of(numberFinite.get().value().longValueExact()); } catch (ArithmeticException exception) { // TODO: more specific error? throw new ConfigError( String.format( "%s must be a 64-bit integer", numberFinite.get().path() ), new FileFragmentSource(numberFinite.get().sourceRange()) ); } } public long getLong(String memberName) { return getLongOrNone(memberName) .orElseThrow(() -> new ConfigError( // TODO: escape member name MessageFormat.format("{0} is missing {1}", object.path(), memberName), new FileFragmentSource(object.sourceRange()) )); } private Optional getValueOfType( Class valueType, String memberName ) { return object.getValue(memberName) .map(value -> castJson5Value( valueType, value )); } private static TValue castJson5Value( Class valueType, Json5Value value ) { try { return valueType.cast(value); } catch (ClassCastException exception) { throw new UnexpectedTypeConfigError( value.path(), valueType, value.getClass(), new FileFragmentSource(value.sourceRange()) ); } } }