Schema.java
/*
* Copyright 2017 Michael Mackenzie High
*
* 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 com.mackenziehigh.sexpr;
import com.mackenziehigh.sexpr.internal.schema.InternalAnnotator;
import com.mackenziehigh.sexpr.internal.schema.InternalSchema;
import com.mackenziehigh.sexpr.internal.schema.InternalSchemaParser;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Predicate;
/**
* An instance of this class can be used to determine
* whether a symbolic-expression matches a pattern.
*/
public final class Schema
{
/**
* Use an instance of this class to create a <code>Schema</code> object.
*/
public static final class Builder
{
/**
* This is the instance that is being constructed.
*/
private final InternalSchema instance = new InternalSchema();
/**
* This flag becomes true, when <code>build()</code> get called.
*/
private volatile boolean built = false;
/**
* Sole Constructor.
*/
private Builder ()
{
// Pass.
}
private void requireNotBuilt ()
{
if (built)
{
throw new IllegalStateException("build() was already called.");
}
}
/*
* This method imports the schema rules defined in the given string.
*
* @param schema is the textual schema.
* @return this.
*/
public Builder include (final String schema)
{
requireNotBuilt();
final StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
final StackTraceElement caller = stackTrace[stackTrace.length - 1];
final String location = caller.toString();
InternalSchemaParser.parse(instance, location, schema);
return this;
}
/**
* This method imports the schema rules defined in the given string.
*
* @param source is a human-readable string indicating where the schema is from.
* @param schema is the textual schema to parse.
* @return this.
*/
public Builder include (final String source,
final String schema)
{
requireNotBuilt();
InternalSchemaParser.parse(instance, source, schema);
return this;
}
/**
* Use this method to define a predicate that can be used within a schema.
*
* <p>
* Inside of the schema, the predicate must be referenced via a 'require' rule.
* </p>
*
* @param name is the name that will be used to identify the predicate.
* @param condition is the predicate itself.
* @return this.
* @throws IllegalArgumentException if the name already identifies a predicate.
*/
public Builder condition (String name,
Predicate<Sexpr<?>> condition)
{
requireNotBuilt();
Objects.requireNonNull(name, "name");
Objects.requireNonNull(condition, "condition");
instance.defineCondition(name, condition);
return this;
}
/**
* Use this method to declare another translation pass.
*
* <p>
* Call this method multiple times in order to declare multiple passes.
* The translation passes will occur in the order of those invocations.
* </p>
*
* @param name is the name of the new translation pass.
* @return this.
*/
public Builder pass (String name)
{
requireNotBuilt();
Objects.requireNonNull(name, "name");
instance.definePass(name);
return this;
}
/**
* Use this method to define an action that will be executed before
* matches of a named rule during a specific pass.
*
* @param pass is the name of the pass that this action applies to.
* @param rule is the name of the rule that this action applies to.
* @param action is the action itself.
* @return this.
* @throws IllegalArgumentException if the name already identifies a action.
*/
public Builder before (String pass,
String rule,
Consumer<Sexpr<?>> action)
{
requireNotBuilt();
Objects.requireNonNull(pass, "pass");
Objects.requireNonNull(rule, "rule");
Objects.requireNonNull(action, "action");
instance.defineBeforeAction(pass, rule, action);
return this;
}
/**
* Use this method to define an action that will be executed after
* matches of a named rule during a specific pass.
*
* @param pass is the name of the pass that this action applies to.
* @param rule is the name of the rule that this action applies to.
* @param action is the action itself.
* @return this.
* @throws IllegalArgumentException if the name already identifies a action.
*/
public Builder after (String pass,
String rule,
Consumer<Sexpr<?>> action)
{
requireNotBuilt();
Objects.requireNonNull(pass, "pass");
Objects.requireNonNull(rule, "rule");
Objects.requireNonNull(action, "action");
instance.defineAfterAction(pass, rule, action);
return this;
}
/**
* Given an object containing properly annotated methods,
* define the conditions and actions defined therein.
*
* @param object contains condition and action definitions.
* @return this.
*/
public Builder defineViaAnnotations (Object object)
{
requireNotBuilt();
final InternalAnnotator annotator = new InternalAnnotator(this);
annotator.defineViaReflection(object);
return this;
}
/**
* Use this method to obtain the new schema object.
*
* @return the new matcher.
*/
public Schema build ()
{
requireNotBuilt();
built = true;
instance.validate();
return new Schema(instance);
}
}
/**
* Result of a <code>match(Sexpr)</code> invocation.
*/
public interface Match
{
/**
* Determine whether this object represents a successful match.
*
* @return true, if the match attempt succeeded.
*/
public boolean isSuccess ();
/**
* Determine whether this object represents a unsuccessful match.
*
* @return true, if the match attempt failed.
*/
public boolean isFailure ();
/**
* Get the symbolic-expression that was the input to <code>match()</code>.
*
* @return the input.
*/
public Sexpr<?> input ();
/**
* Get the last node of the input that was successfully matched, if any,
* which is useful for locating and reporting errors in the input.
*
* @return the last successfully matched node, if any.
*/
public Optional<Sexpr<?>> lastSuccess ();
/**
* Execute the defined passes and related actions.
*
* <p>
* If the match is successful, then the match-tree
* will be traversed once for each translation-pass
* that was defined previously. Upon encountering
* the successful match of a rule (R) during a
* translation-pass, the before-actions of (R)
* will be executed, then the subordinate matches
* will be visited and their actions will be executed,
* and then the after-actions of (R) will be executed.
* </p>
*
* @return this.
*/
public Match execute ();
}
private final InternalSchema internal;
private Schema (final InternalSchema internal)
{
this.internal = internal;
}
/**
* This method determines whether the given symbolic-expression obeys this schema.
*
* @param input is the symbolic-expression that this schema may match.
* @return an object that describes the whether the match was successful or not.
*/
public Match match (final Sexpr<?> input)
{
return internal.match(input);
}
/**
* Builder Factory.
*
* @return an object that can be used to build a <code>Schema</code> object.
*/
public static Builder newBuilder ()
{
return new Builder();
}
}