2020-02-05 12:10:07 +02:00
|
|
|
[Requirements::] Requirements.
|
|
|
|
|
|
|
|
A requirement is a way to specify some subset of works: for example, those
|
|
|
|
with a given title, and/or version number.
|
|
|
|
|
2020-03-29 19:39:17 +03:00
|
|
|
@h Creation.
|
|
|
|
A requirement is, in effect, the criteria for performing a search. We can
|
|
|
|
specify the title, and/or the author name, and/or the genre -- all given
|
|
|
|
in the |work| field below, with those unspecified left blank -- and/or
|
|
|
|
we can give a semantic version number range:
|
2020-02-05 12:10:07 +02:00
|
|
|
|
|
|
|
=
|
|
|
|
typedef struct inbuild_requirement {
|
|
|
|
struct inbuild_work *work;
|
2020-03-02 14:55:33 +02:00
|
|
|
struct semver_range *version_range;
|
2020-05-09 15:07:39 +03:00
|
|
|
CLASS_DEFINITION
|
2020-02-05 12:10:07 +02:00
|
|
|
} inbuild_requirement;
|
|
|
|
|
2020-03-29 19:39:17 +03:00
|
|
|
@ Here are some creators:
|
|
|
|
|
|
|
|
=
|
2020-03-02 14:55:33 +02:00
|
|
|
inbuild_requirement *Requirements::new(inbuild_work *work, semver_range *R) {
|
2020-02-05 12:10:07 +02:00
|
|
|
inbuild_requirement *req = CREATE(inbuild_requirement);
|
|
|
|
req->work = work;
|
2020-03-02 14:55:33 +02:00
|
|
|
req->version_range = R;
|
2020-02-05 12:10:07 +02:00
|
|
|
return req;
|
|
|
|
}
|
|
|
|
|
|
|
|
inbuild_requirement *Requirements::any_version_of(inbuild_work *work) {
|
2020-03-23 00:45:46 +02:00
|
|
|
return Requirements::new(work, VersionNumberRanges::any_range());
|
2020-02-05 12:10:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
inbuild_requirement *Requirements::anything_of_genre(inbuild_genre *G) {
|
|
|
|
return Requirements::any_version_of(Works::new(G, I"", I""));
|
|
|
|
}
|
|
|
|
|
|
|
|
inbuild_requirement *Requirements::anything(void) {
|
|
|
|
return Requirements::anything_of_genre(NULL);
|
|
|
|
}
|
|
|
|
|
2020-03-29 19:39:17 +03:00
|
|
|
@ The most involved of the creators parses text. An involved example might be:
|
2020-04-08 01:02:44 +03:00
|
|
|
= (text)
|
|
|
|
genre=extension,author=Emily Short,title=Locksmith,min=6.1-alpha.2,max=17.2
|
|
|
|
=
|
2020-03-29 19:39:17 +03:00
|
|
|
We should return a requirement if this is valid, and write an error message if
|
|
|
|
it is not. (If the text has multiple things wrong with it, we write only the
|
|
|
|
first error message arising.)
|
|
|
|
|
|
|
|
At the top level, we have a comma-separated list of clauses. Note that the
|
|
|
|
empty text is legal here, and produces an unlimited requirement.
|
|
|
|
|
|
|
|
=
|
|
|
|
inbuild_requirement *Requirements::from_text(text_stream *T,
|
|
|
|
text_stream *errors) {
|
2020-02-05 12:10:07 +02:00
|
|
|
inbuild_requirement *req = Requirements::anything();
|
|
|
|
int from = 0;
|
|
|
|
for (int at = 0; at < Str::len(T); at++) {
|
2023-09-05 10:36:51 +03:00
|
|
|
inchar32_t c = Str::get_at(T, at);
|
2020-02-05 12:10:07 +02:00
|
|
|
if (c == ',') {
|
2020-06-28 01:18:54 +03:00
|
|
|
TEMPORARY_TEXT(initial)
|
2020-02-05 12:10:07 +02:00
|
|
|
Str::substr(initial, Str::at(T, from), Str::at(T, at));
|
|
|
|
Requirements::impose_clause(req, initial, errors);
|
2020-06-28 01:18:54 +03:00
|
|
|
DISCARD_TEXT(initial)
|
2020-02-05 12:10:07 +02:00
|
|
|
from = at + 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (from < Str::len(T)) {
|
2020-06-28 01:18:54 +03:00
|
|
|
TEMPORARY_TEXT(final)
|
2020-02-05 12:10:07 +02:00
|
|
|
Str::substr(final, Str::at(T, from), Str::end(T));
|
|
|
|
Requirements::impose_clause(req, final, errors);
|
2020-06-28 01:18:54 +03:00
|
|
|
DISCARD_TEXT(final)
|
2020-02-05 12:10:07 +02:00
|
|
|
}
|
|
|
|
return req;
|
|
|
|
}
|
|
|
|
|
2020-03-29 19:39:17 +03:00
|
|
|
@ Each clause must either be |all| or take the form |term=value|:
|
|
|
|
|
|
|
|
=
|
|
|
|
void Requirements::impose_clause(inbuild_requirement *req, text_stream *T,
|
|
|
|
text_stream *errors) {
|
2020-02-05 12:10:07 +02:00
|
|
|
Str::trim_white_space(T);
|
|
|
|
if (Str::eq(T, I"all")) return;
|
|
|
|
|
2020-06-28 01:18:54 +03:00
|
|
|
TEMPORARY_TEXT(term)
|
|
|
|
TEMPORARY_TEXT(value)
|
2020-02-05 12:10:07 +02:00
|
|
|
for (int at = 0; at < Str::len(T); at++) {
|
2023-09-05 10:36:51 +03:00
|
|
|
inchar32_t c = Str::get_at(T, at);
|
2020-02-05 12:10:07 +02:00
|
|
|
if (c == '=') {
|
2020-03-29 19:39:17 +03:00
|
|
|
Str::substr(term, Str::start(T), Str::at(T, at));
|
2020-02-05 12:10:07 +02:00
|
|
|
Str::substr(value, Str::at(T, at+1), Str::end(T));
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2020-03-29 19:39:17 +03:00
|
|
|
Str::trim_white_space(term);
|
2020-02-05 12:10:07 +02:00
|
|
|
Str::trim_white_space(value);
|
2020-03-29 19:39:17 +03:00
|
|
|
if ((Str::len(term) > 0) && (Str::len(value) > 0)) {
|
|
|
|
@<Deal with a term-value pair@>;
|
2020-02-05 12:10:07 +02:00
|
|
|
} else {
|
|
|
|
if (Str::len(errors) == 0)
|
|
|
|
WRITE_TO(errors, "clause not in the form 'term=value': '%S'", T);
|
|
|
|
}
|
2020-06-28 01:18:54 +03:00
|
|
|
DISCARD_TEXT(term)
|
|
|
|
DISCARD_TEXT(value)
|
2020-02-05 12:10:07 +02:00
|
|
|
}
|
|
|
|
|
2020-03-29 19:39:17 +03:00
|
|
|
@<Deal with a term-value pair@> =
|
|
|
|
if (Str::eq(term, I"genre")) {
|
|
|
|
inbuild_genre *G = Genres::by_name(value);
|
|
|
|
if (G) req->work->genre = G;
|
|
|
|
else if (Str::len(errors) == 0)
|
|
|
|
WRITE_TO(errors, "not a valid genre: '%S'", value);
|
|
|
|
} else if (Str::eq(term, I"title")) {
|
|
|
|
Str::copy(req->work->title, value);
|
|
|
|
} else if (Str::eq(term, I"author")) {
|
|
|
|
Str::copy(req->work->author_name, value);
|
|
|
|
} else if (Str::eq(term, I"version")) {
|
|
|
|
semantic_version_number V = Requirements::semver(value, errors);
|
|
|
|
req->version_range = VersionNumberRanges::compatibility_range(V);
|
|
|
|
} else if (Str::eq(term, I"min")) {
|
|
|
|
semantic_version_number V = Requirements::semver(value, errors);
|
|
|
|
req->version_range = VersionNumberRanges::at_least_range(V);
|
|
|
|
} else if (Str::eq(term, I"max")) {
|
|
|
|
semantic_version_number V = Requirements::semver(value, errors);
|
|
|
|
req->version_range = VersionNumberRanges::at_most_range(V);
|
|
|
|
} else {
|
|
|
|
if (Str::len(errors) == 0)
|
|
|
|
WRITE_TO(errors, "no such term as '%S'", term);
|
|
|
|
}
|
|
|
|
|
|
|
|
@ =
|
|
|
|
semantic_version_number Requirements::semver(text_stream *value, text_stream *errors) {
|
|
|
|
semantic_version_number V = VersionNumbers::from_text(value);
|
|
|
|
if (VersionNumbers::is_null(V))
|
|
|
|
if (Str::len(errors) == 0)
|
|
|
|
WRITE_TO(errors, "not a valid version number: '%S'", value);
|
|
|
|
return V;
|
|
|
|
}
|
|
|
|
|
|
|
|
@h Writing.
|
|
|
|
This is the inverse of the above function, and uses the same notation.
|
|
|
|
|
|
|
|
=
|
2020-02-13 01:48:37 +02:00
|
|
|
void Requirements::write(OUTPUT_STREAM, inbuild_requirement *req) {
|
|
|
|
if (req == NULL) { WRITE("<none>"); return; }
|
|
|
|
int claused = FALSE;
|
|
|
|
if (req->work->genre) {
|
|
|
|
if (claused) WRITE(","); claused = TRUE;
|
|
|
|
WRITE("genre=%S", req->work->genre->genre_name);
|
|
|
|
}
|
|
|
|
if (Str::len(req->work->title) > 0) {
|
|
|
|
if (claused) WRITE(","); claused = TRUE;
|
|
|
|
WRITE("work=%S", req->work->title);
|
|
|
|
}
|
|
|
|
if (Str::len(req->work->author_name) > 0) {
|
|
|
|
if (claused) WRITE(","); claused = TRUE;
|
|
|
|
WRITE("author=%S", req->work->author_name);
|
|
|
|
}
|
2020-03-23 00:45:46 +02:00
|
|
|
if (VersionNumberRanges::is_any_range(req->version_range) == FALSE) {
|
2020-02-13 01:48:37 +02:00
|
|
|
if (claused) WRITE(","); claused = TRUE;
|
2020-03-23 00:45:46 +02:00
|
|
|
WRITE("range="); VersionNumberRanges::write_range(OUT, req->version_range);
|
2020-02-13 01:48:37 +02:00
|
|
|
}
|
|
|
|
if (claused == FALSE) WRITE("all");
|
|
|
|
}
|
|
|
|
|
2020-03-29 19:39:17 +03:00
|
|
|
@h Meeting requirements.
|
|
|
|
Finally, we actually use these intricacies for something. Given an edition,
|
|
|
|
we return |TRUE| if it meets the requirements and |FALSE| if it does not.
|
|
|
|
|
|
|
|
Note that requirements are based on the edition, not on the copy. If one
|
|
|
|
copy on file of Version 3.2 of Monkey Puzzle Trees by Capability Brown meets
|
|
|
|
a requirement, then so will all other copies of it.
|
|
|
|
|
|
|
|
=
|
2020-02-05 12:10:07 +02:00
|
|
|
int Requirements::meets(inbuild_edition *edition, inbuild_requirement *req) {
|
|
|
|
if (req == NULL) return TRUE;
|
|
|
|
if (req->work) {
|
2020-03-29 19:39:17 +03:00
|
|
|
if (req->work->genre)
|
2022-12-08 01:28:26 +02:00
|
|
|
if (Genres::equivalent(req->work->genre, edition->work->genre) == FALSE)
|
2020-02-05 12:10:07 +02:00
|
|
|
return FALSE;
|
2020-03-29 19:39:17 +03:00
|
|
|
if (Str::len(req->work->title) > 0)
|
2020-02-05 12:10:07 +02:00
|
|
|
if (Str::ne_insensitive(req->work->title, edition->work->title))
|
|
|
|
return FALSE;
|
2020-03-29 19:39:17 +03:00
|
|
|
if (Str::len(req->work->author_name) > 0)
|
2020-02-05 12:10:07 +02:00
|
|
|
if (Str::ne_insensitive(req->work->author_name, edition->work->author_name))
|
|
|
|
return FALSE;
|
|
|
|
}
|
2020-03-23 00:45:46 +02:00
|
|
|
return VersionNumberRanges::in_range(edition->version, req->version_range);
|
2020-02-05 12:10:07 +02:00
|
|
|
}
|
2022-04-05 14:14:27 +03:00
|
|
|
|
|
|
|
@ This is a very weak form of testing that requirement |A| is stronger than
|
|
|
|
requirement |B| concerning the same work; it only catches the case where |B|
|
|
|
|
imposes no version constraints.
|
|
|
|
|
|
|
|
=
|
|
|
|
int Requirements::trumps(inbuild_requirement *A, inbuild_requirement *B) {
|
|
|
|
if (B == NULL) return TRUE;
|
|
|
|
if (A == NULL) return FALSE;
|
|
|
|
if (B->work) {
|
|
|
|
if (B->work->genre)
|
|
|
|
if (B->work->genre != A->work->genre)
|
|
|
|
return FALSE;
|
|
|
|
if (Str::len(B->work->title) > 0)
|
|
|
|
if (Str::ne_insensitive(B->work->title, A->work->title))
|
|
|
|
return FALSE;
|
|
|
|
if (Str::len(B->work->author_name) > 0)
|
|
|
|
if (Str::ne_insensitive(B->work->author_name, A->work->author_name))
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
if (VersionNumberRanges::is_any_range(B->version_range)) return TRUE;
|
|
|
|
return FALSE;
|
|
|
|
}
|