Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Descriptor and validation tools #10

Closed
20 changes: 20 additions & 0 deletions apiguardian-contract/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'

repositories {
mavenCentral()
}

compileJava {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}

compileTestJava {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}

//todo: fill the rest of buildscript in a way that matches apiguardian-report
//todo: proper tests, though frankly I can't see any other way to test this than to write down all possible graph transitions
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package org.apiguardian.contract;

/**
* Describes state of the API element (a feature) in context of some version.
* Explicitly maps to org.apiguardian.api.API.Status and extends status set with NONE.
* <p>
* There is no mapping method (that would turn mentioned enum to this one) to avoid dependency on API module.
*/
public enum ApiElementState {
/**
* Feature is not present in analysed version.
*/
NONE,
/**
* Feature is described as o.a.a.API.Status.INTERNAL in analysed version.
*/
INTERNAL,
/**
* Feature is described as o.a.a.API.Status.DEPRECATED in analysed version.
*/
DEPRECATED,
/**
* Feature is described as o.a.a.API.Status.EXPERIMENTAL in analysed version.
*/
EXPERIMENTAL,
/**
* Feature is described as o.a.a.API.Status.MAINTAINED in analysed version.
*/
MAINTAINED,
/**
* Feature is described as o.a.a.API.Status.STABLE in analysed version.
*/
STABLE;

/**
* Is the feature available in analysed version?
* @return true if feature is available (with any status) in analysed version, false in other case
*/
public boolean featureExists(){
switch (this){
case NONE: return false;
default: return true;
}
}

/**
* Should this feature be used when solving a new case?
* @return true if feature exists and is not internal or deprecated, false in other case
*/
public boolean canBeSafelyUsed(){
switch (this){
case INTERNAL:
case DEPRECATED: return false;
default: return featureExists();
}
}


/**
* Is this feature expected to still exist in next minor version?
* @return true if the feature is maintained or stable, false in other case
*/
//todo better name - suggestions welcome
public boolean willNotDisappear(){
switch (this){
case MAINTAINED:
case STABLE: return true;
default: return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package org.apiguardian.contract;

import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.Arrays.asList;
import static org.apiguardian.contract.ApiElementState.*;
import static org.apiguardian.contract.StateTransitionRule.*;

/**
* Class containing static methods for analysing API state graph.
* Internally holds static graph description and is able to query it to answer questions on transition validity
* or possible transitions.
*/
public class ApiVersioningContract {
private ApiVersioningContract() {
}

private static final Collection<ApiElementState> ALL_STATES = asList(ApiElementState.values());

public static List<StateTransitionRule> rules = asList(
onMajorVersionIncrement(ALL_STATES, ALL_STATES),
anytime(asList(INTERNAL, EXPERIMENTAL), NONE),
onMinorVersionIncrement(DEPRECATED, NONE),
anytime(EXPERIMENTAL, asList(DEPRECATED, MAINTAINED, STABLE)),
onMinorVersionIncrement(MAINTAINED, DEPRECATED),
anytime(MAINTAINED, STABLE),
anytime(NONE, ALL_STATES)
);

/**
* Is given transition valid?
* @param previousState State of the feature in previous analysed version
* @param nextState State of the feature in next analysed version
* @param versionComponentChange Most general change in version components
* @see VersionComponentChange
* @return true if transition of feature state is valid, false in other case
*/
public static boolean isValidTransition(ApiElementState previousState, ApiElementState nextState,
VersionComponentChange versionComponentChange){
return (previousState == nextState) || rules.stream().
map(rule ->
rule.isSatisfied(previousState, nextState, versionComponentChange)
).filter(x -> x).
findAny().
isPresent();
}

/**
* What state can a feature have in next version that differs from previous version by given component change?
* @param previousState State of the feature in previous analysed version
* @param versionComponentChange Most general change in version components
* @see VersionComponentChange
* @return Set of valid states in which a feature can be in next version
*/
public static Set<ApiElementState> findValidNextStates(ApiElementState previousState,
VersionComponentChange versionComponentChange){
return Stream.of(ApiElementState.values()).
filter(state ->
isValidTransition(previousState, state, versionComponentChange)
).
collect(Collectors.toSet());
}

/**
* What state could a feature be in previously, if it is in given state in next version that differs from previous
* one by given component change?
* @param nextState State of the feature in currently analysed version
* @param versionComponentChange Most general change in version components
* @see VersionComponentChange
* @return Set of valid states in which a feature could be in previous version
*/
public static Set<ApiElementState> findValidPreviousStates(ApiElementState nextState,
VersionComponentChange versionComponentChange){
return Stream.of(ApiElementState.values()).
filter(state ->
isValidTransition(state, nextState, versionComponentChange)
).
collect(Collectors.toSet());
}

/**
* What is the most specific version change so that transition between feature states would be valid?
* @param previousState State of the feature in previous analysed version
* @param nextState State of the feature in next analysed version
* @return Most specific change in version components for a transition to be valid
*/
public static VersionComponentChange findMostSpecificChangeForValidTransition(ApiElementState previousState,
ApiElementState nextState){
return Stream.of(VersionComponentChange.values()).filter(change ->
isValidTransition(previousState, nextState, change)
).sorted(
Comparator.<VersionComponentChange>naturalOrder().reversed() //sort from most specific to most general
).findFirst().get();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package org.apiguardian.contract;

import java.util.Collection;
import java.util.function.Predicate;

import static java.util.Arrays.asList;

/**
* Single rule describing allowed API element state transitions between versions.
* Package-private, because it shouldn't be used by 3rd parties, but rather wrapped in graph-defining class used by them.
* <p>
* Use utility factory methods to create instances for readability.
*/
final class StateTransitionRule {
private Predicate<ApiElementState> previousStatePredicate;
private Predicate<ApiElementState> nextStatePredicate;
private VersionComponentChange requiredVersionComponentChange;

private StateTransitionRule(Predicate<ApiElementState> previousStatePredicate,
Predicate<ApiElementState> nextStatePredicate,
VersionComponentChange requiredVersionComponentChange) {
this.previousStatePredicate = previousStatePredicate;
this.nextStatePredicate = nextStatePredicate;
this.requiredVersionComponentChange = requiredVersionComponentChange;
}

/**
* If the transition between states allowed?
* @param previousState State of a feature in previous analysed version
* @param nextState State of a feature in next analysed version
* @param versionComponentChange Most general change between analysed versions
* @see VersionComponentChange
* @return true if previous and next state predicates match arguments and major and minor version components changed
* according to requirements
*/
public boolean isSatisfied(ApiElementState previousState, ApiElementState nextState,
VersionComponentChange versionComponentChange){
return previousStatePredicate.test(previousState) && nextStatePredicate.test(nextState) &&
versionComponentChange.compareTo(requiredVersionComponentChange) >=0;
}

static StateTransitionRule anytime(ApiElementState previousState, ApiElementState nextState){
return anytime(asList(previousState), asList(nextState));
}

static StateTransitionRule anytime(ApiElementState previousState, Collection<ApiElementState> nextStates){
return anytime(asList(previousState), nextStates);
}

static StateTransitionRule anytime(Collection<ApiElementState> previousStates, ApiElementState nextState){
return anytime(previousStates, asList(nextState));
}

static StateTransitionRule anytime(Collection<ApiElementState> previousStates,
Collection<ApiElementState> nextStates){
return anytime(previousStates::contains, nextStates::contains);
}

static StateTransitionRule anytime(Predicate<ApiElementState> previousStatePredicate,
Predicate<ApiElementState> nextStatePredicate){
return new StateTransitionRule(previousStatePredicate, nextStatePredicate, VersionComponentChange.NONE);
}

static StateTransitionRule onMajorVersionIncrement(Collection<ApiElementState> previousStates,
Collection<ApiElementState> nextStates){
return onMajorVersionIncrement(previousStates::contains, nextStates::contains);
}

static StateTransitionRule onMajorVersionIncrement(Predicate<ApiElementState> previousStatePredicate,
Predicate<ApiElementState> nextStatePredicate){
return new StateTransitionRule(previousStatePredicate, nextStatePredicate, VersionComponentChange.MAJOR);
}

static StateTransitionRule onMinorVersionIncrement(ApiElementState previousState, ApiElementState nextState){
return onMinorVersionIncrement(previousState::equals, nextState::equals);
}

static StateTransitionRule onMinorVersionIncrement(Predicate<ApiElementState> previousStatePredicate,
Predicate<ApiElementState> nextStatePredicate){
return new StateTransitionRule(previousStatePredicate, nextStatePredicate, VersionComponentChange.MINOR);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package org.apiguardian.contract;

/**
* Enumeration describing the difference between two versions, assumed both are compliant with basic Semantic Versioning
* format. Labels and other metadata (like snapshot, pre-release, etc) are ignored.
* <p>
* <strong>Comparability</strong><br/>
* Enum values are ordered from more specific to more general. If a version has changed in two ways, use the more
* general one (e.g. if version 1.0.0 has changed to 2.1.0, do NOT use <code>MINOR</code>, but rather <code>MAJOR</code>).
* In other words, use the value that describes first component change when reading versions from left to right.
* <br/>
* <code>Comparable<...></code> is implemented here in that way - given <code>VersionComponentChange c1</code> and
* <code>VersionComponentChange c2</code>, then <code>c1.compareTo(c2) > 0</code>, meaning that <code>c1</code> is more
* general than <code>c2</code>, e.g. <code>c1 = MAJOR</code> and <code>c2 = MINOR</code>.
* @see <a href="https://semver.org/">Semantic Versioning</a>
*/
//todo is "most specific" and "most general" good naming for version change "size"?
// it seems quite natural to say that MAJOR version change is more general than MINOR, but I'm not sure whether
// MINOR is more specific than MAJOR; still, I can't find a better phrasing for now
// would that change, remember to propagate it to ApiVersioningContract javadocs too
public enum VersionComponentChange {
/**
* No version component has changed.
* <p>
* This does not mean that versions are equal - just that no component has changed.
* <p>
* Example:
* <ul>
* <li>1.0.0 \u21e8 1.0.0</li>
* <li>0.0.1-SNAPSHOT \u21e8 0.0.1</li>
* </ul>
*/
NONE,
/**
* Minor version component has changed.
* <p>
* Examples:
* <ul>
* <li>1.0.0 \u21e8 1.0.1</li>
* <li>0.0.1 \u21e8 0.0.2</li>
* </ul>
*/
PATCH,
/**
* Minor version component has changed.
* <p>
* Examples:
* <ul>
* <li>2.0.0 \u21e8 2.1.0</li>
* <li>1.4.1 \u21e8 1.5.2</li>
* <li>2.5.5 \u21e8 2.6.0</li>
* <li>0.0.1 \u21e8 0.1.0</li>
* </ul>
*/
MINOR,
/**
* Major version component has changed.
* <p>
* Examples:
* <ul>
* <li>2.0.0 \u21e8 3.0.0</li>
* <li>1.4.1 \u21e8 2.0.0</li>
* <li>2.5.0 \u21e8 3.1.0</li>
* <li>0.1.0 \u21e8 1.0.0</li>
* </ul>
*/
MAJOR
}
51 changes: 51 additions & 0 deletions apiguardian-describer/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'

repositories {
mavenCentral()
}

compileJava {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}

compileTestJava {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}

dependencies {
compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.16.20'
testCompileOnly group: 'org.projectlombok', name: 'lombok', version: '1.16.20'

compile group: 'org.reflections', name: 'reflections', version: '0.9.11'

compile project(":");
compile project(":apiguardian-descriptor-model")

//todo migrate to junit5
testCompile group: 'junit', name: 'junit', version: '4.12'
//todo it would be perfect if artifact to be described wouldnt have to be on classpath
testCompile project(":apiguardian-descriptor-example")

}

def exampleJarTask = (project(":apiguardian-descriptor-example").getTasksByName("jar", false) as List)[0]

task(writeExampleJarPathToResources){
doFirst {
def f = file("src/test/resources/example_path.txt")
if (!f.parentFile.exists())
f.parentFile.mkdirs()
f.text = (exampleJarTask as Jar).archivePath.absolutePath
}
}

test.dependsOn(exampleJarTask, writeExampleJarPathToResources)

//todo add internal classes to descriptor
//todo javadocs
//todo more extensive testing
//todo proper submodules buildscripts
Loading