Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,38 @@ are expected and listed without apology.

## Unreleased

- `@paged(key:page:)`, the third directive that is not Relay's: a list
paged by number, as every list on AniList is, says so on the list field,
inside the field that takes the page, as in `Page(page: $page, perPage:
50) { pageInfo { hasNextPage } media(season: $season) @paged(key:
"SeasonQuery_media", page: $page) { ... } }`. The pages merge into a
client record of the container's type on the container's parent, keyed
by the list field and its arguments with the container's other
arguments, so two lists under one wrapper, two seasons of one list and
two page sizes never share it, and its page info is its own record: the
shared `pageInfo` slot that made one list read another's flag is not
read. The first page replaces, the next appends, a page already merged
rewrites its elements in place, any other is ignored; elements are
referenced, each record once. The container's lens gains `hasNext`,
which reads `hasNextPage` and nothing else, `isLoadingNext`, and
`loadNext()`, which runs the operation that owns the lens with its
variables and the next page; no `@refetchable` is needed, and a refetch
of the owner replaces the list. Both runtimes. The compiler declares the
directive beside `@cacheExpiration`, so Relay's front end checks its
arguments and a server never sees it, and reports at the directive: a
constant page, a fragment argument, a nullable variable, a key that does
not end in the field's name, a scalar, a field above that does not take
the page, a container without `pageInfo { hasNextPage }`. Swift format
21, Kotlin format 5: generated code of the format before fails to
compile at its marker; rebuild with this release's compiler. Held by the
cases `paged-notes-1` to `-3`, `paged-characters-1`, `friends-1` and
`friends-2` and the script `paged`
([the decision](docs/decisions/a-paged-list-merges-its-pages-by-number.md),
[#49](https://github.com/shergin/baton/issues/49)). Not chosen: offsets,
which wait for a real schema; a previous page; edge directives into a
paged list; a second reading of the flag for a page info that is not
`pageInfo { hasNextPage }`, so the Rick and Morty API's lists keep
reading one page per operation value.
- Four recipes for a public API with habits of its own, asked for on
[#52](https://github.com/shergin/baton/issues/52), each in Swift and
Kotlin and compiled against AniList's schema. None is a new concept.
Expand Down
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,26 @@ fun IssueList(repository: IssueList_repository) {
</tr>
</table>

A list paged by number, as every list on AniList is, says so on the list
field, inside the field that takes the page, with `@paged(key:, page:)`.
The pages merge in the store under the list and its arguments, apart from
the wrapper the server puts around them, and the wrapper's lens gains
`hasNext`, `isLoadingNext` and `loadNext()`, which runs the screen's own
query with the next page:

```graphql
query SeasonQuery($page: Int!, $season: MediaSeason) {
Page(page: $page, perPage: 50) {
pageInfo { hasNextPage }
media(season: $season, type: ANIME, sort: [POPULARITY_DESC])
@paged(key: "SeasonQuery_media", page: $page) {
id
...MediaCard_media
}
}
}
```

A Kotlin document is a `$$"""…"""` string, since `$` starts a template in a
plain one and GraphQL's variables need it. The view marker stands on the
composable, and the composable remembers the operation value it shows.
Expand Down Expand Up @@ -343,7 +363,7 @@ A screen written by a language model has the same shape as one written by a
person, and goes through the same checks. The fragment beside the view is
complete, valid GraphQL; the schema decides what exists; the generated lens
decides what the body may read; the directives decide how data moves
(`@connection`, `@appendEdge`, `@deletes`, `@required`, `@catch`). There is
(`@connection`, `@paged`, `@appendEdge`, `@deletes`, `@required`, `@catch`). There is
no cache policy to configure, no normalizer to teach and no updater function
to write, so there is nothing a generator has to know that is not in the
file in front of it. The vocabulary is closed and short
Expand Down
14 changes: 9 additions & 5 deletions compiler/src/decide.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,17 @@ pub use collect::{InputField, Shared};
pub use keys::{KeyPart, SlotRef, constant_json};
pub use lens::{
Accessor, AliasGuard, AliasedRead, Binding, BoundArgument, ConditionRead, ConnectionMembers,
ErrorCheck, ErrorLine, Guarded, LinkedForm, LinkedRead, ListShape, LoadMore, Primitive, Read,
ReaderPlan, RefetchMembers, SatisfiedCheck, ScalarForm, ScalarRead, ScalarShape, SlotAccess,
SpreadForm, SpreadGuard, SpreadRead, TypeTest,
ErrorCheck, ErrorLine, Guarded, LinkedForm, LinkedRead, ListShape, LoadMore, PagedMembers,
Primitive, Read, ReaderPlan, RefetchMembers, SatisfiedCheck, ScalarForm, ScalarRead,
ScalarShape, SlotAccess, SpreadForm, SpreadGuard, SpreadRead, TypeTest,
};
pub use operation::{BuilderPlan, BuilderValue, OperationValue, VariableBase, VariableValue};
pub use selections::SelectionTable;

use crate::naming::{NameError, Naming, Position, Reserved, Written};
use crate::pipeline::{
ConditionClass, ConnectionPlan, EditPlan, LookupPlan, Plan, SelectionPlan, StorageKeyPlan,
TypePlan,
ConditionClass, ConnectionPlan, EditPlan, LookupPlan, PagedPlan, Plan, SelectionPlan,
StorageKeyPlan, TypePlan,
};

/// Everything the emitters print, in the plan's order.
Expand Down Expand Up @@ -277,6 +277,8 @@ pub enum NormalizationKind {
plural: bool,
lookup: Option<LookupPlan>,
connection: Option<ConnectionPlan>,
/// The paged list this field is the container of.
paged: Option<PagedPlan>,
selection: NormalizationSelection,
},
}
Expand Down Expand Up @@ -656,6 +658,7 @@ fn field(
possible_types,
lookup,
connection,
paged,
streamed,
..
} => {
Expand Down Expand Up @@ -693,6 +696,7 @@ fn field(
plural: type_.is_list(),
lookup: lookup.clone(),
connection: connection.clone(),
paged: paged.clone(),
selection: decide(
type_.base_name(),
keys,
Expand Down
13 changes: 13 additions & 0 deletions compiler/src/decide/collect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ impl Shared {
self.types.insert(connection.edge_type.clone());
self.types.insert(connection.page_info_type.clone());
}
if let Some(paged) = &lens.paged {
self.types.insert(paged.page_info_type.clone());
}
for entry in lens.satisfied.iter().flatten() {
match &entry.item {
Some(
Expand Down Expand Up @@ -243,6 +246,7 @@ impl Shared {
let NormalizationKind::Linked {
lookup,
connection,
paged,
selection: child,
..
} = &field.kind
Expand All @@ -267,6 +271,15 @@ impl Shared {
self.types.insert(connection.edge_type.clone());
self.types.insert(connection.page_info_type.clone());
}
if let Some(paged) = paged {
// The client key on the parent, and the list's key on
// the container, which the merge finds the list by.
self.slots
.insert(SlotRef::new(slot_type, &paged.storage_key));
self.slots
.insert(SlotRef::new(&child.type_name, &paged.list));
self.types.insert(paged.page_info_type.clone());
}
self.selection(child);
}
}
Expand Down
16 changes: 14 additions & 2 deletions compiler/src/decide/lens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ pub struct ReaderPlan {
pub refetch: Option<RefetchMembers>,
/// A `@connection` field's lens: the connection state and pagination.
pub connection: Option<ConnectionMembers>,
/// A paged list's container: `hasNext`, `isLoadingNext` and `loadNext()`.
pub paged: Option<PagedMembers>,
/// `satisfied`, when a required child can null the lens: one entry per
/// own member, a check when it has one.
pub satisfied: Option<Vec<Guarded<Option<SatisfiedCheck>>>>,
Expand Down Expand Up @@ -96,10 +98,10 @@ impl ReaderPlan {
}
}
}
if self.connection.is_some() {
if self.connection.is_some() || self.paged.is_some() {
names.insert(Spelled::Types);
}
if self.refetch.is_some() || self.connection.is_some() {
if self.refetch.is_some() || self.connection.is_some() || self.paged.is_some() {
names.insert(Spelled::OwnType);
}
for entry in self.satisfied.iter().flatten() {
Expand Down Expand Up @@ -602,6 +604,16 @@ pub struct ConnectionMembers {
pub load_previous: Option<LoadMore>,
}

/// The paging surface of a lens over a paged list's container: the slots
/// of the container and its page info, and the variable `loadNext()`
/// advances on the operation that owns the lens.
#[derive(Debug, Clone, PartialEq)]
pub struct PagedMembers {
pub container_type: String,
pub page_info_type: String,
pub page: String,
}

/// `nodes`: the lenses it reads, as their accessors named them.
#[derive(Debug, Clone, PartialEq)]
pub struct Nodes {
Expand Down
35 changes: 30 additions & 5 deletions compiler/src/decide/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ use std::collections::{BTreeMap, BTreeSet};
use super::checks::{field_errors, is_present, report_missing, satisfied};
use super::lens::{
Accessor, AliasGuard, AliasedRead, Binding, BoundArgument, ConditionRead, ConnectionMembers,
ErrorCheck, ErrorLine, Guarded, LinkedForm, LinkedRead, LoadMore, Nodes, Read, ReaderPlan,
RefetchMembers, ScalarForm, ScalarRead, ScalarShape, SlotAccess, SpreadForm, SpreadGuard,
SpreadRead, TypeTest,
ErrorCheck, ErrorLine, Guarded, LinkedForm, LinkedRead, LoadMore, Nodes, PagedMembers, Read,
ReaderPlan, RefetchMembers, ScalarForm, ScalarRead, ScalarShape, SlotAccess, SpreadForm,
SpreadGuard, SpreadRead, TypeTest,
};
use super::members::{
Member, collect_caught, collect_deferred, condition_lens, derived_spread, members,
Expand All @@ -26,8 +26,8 @@ use super::members::{
use super::written_key;
use crate::naming::{Kind, NameError, Naming, Position, Reserved, Scope, Spelled};
use crate::pipeline::{
ArgumentPlan, CatchTarget, ConditionClass, ConnectionPlan, ConstantPlan, FragmentPlan, Plan,
RefetchPlan, RequiredAction, SelectionPlan, StorageKeyPlan, VariablePlan,
ArgumentPlan, CatchTarget, ConditionClass, ConnectionPlan, ConstantPlan, FragmentPlan,
PagedPlan, Plan, RefetchPlan, RequiredAction, SelectionPlan, StorageKeyPlan, VariablePlan,
};

/// What a spread accessor needs to know about the fragment it produces.
Expand Down Expand Up @@ -125,6 +125,8 @@ struct Nested {
is_abstract: bool,
selections: Vec<SelectionPlan>,
connection: Option<ConnectionPlan>,
/// The paged list the field is the container of, when one is.
paged: Option<PagedPlan>,
bubbles: bool,
within_catch: bool,
/// The response key the lens is read under, for a linked field; an
Expand All @@ -141,6 +143,8 @@ struct LensFacts {
connection: bool,
/// A connection's lens that selects `edges { node }`: `nodes`.
nodes: bool,
/// A paged list's container: `paged` and the paging state.
paged: bool,
}

impl<'a> Readers<'a> {
Expand Down Expand Up @@ -238,6 +242,7 @@ impl<'a> Readers<'a> {
&fragment.reader,
context,
None,
None,
true,
fragment.bubbles,
)
Expand Down Expand Up @@ -265,6 +270,7 @@ impl<'a> Readers<'a> {
&operation.reader,
context,
None,
None,
false,
operation.bubbles,
);
Expand All @@ -290,6 +296,7 @@ impl<'a> Readers<'a> {
selections: &[SelectionPlan],
context: Context<'_>,
connection: Option<&ConnectionPlan>,
paged: Option<&PagedPlan>,
is_fragment_root: bool,
bubbles: bool,
) -> ReaderPlan {
Expand All @@ -298,6 +305,7 @@ impl<'a> Readers<'a> {
refetchable: is_fragment_root && context.refetch.is_some(),
connection: connection.is_some(),
nodes: connection.is_some() && selects_nodes(selections),
paged: paged.is_some(),
};
let duplicates = self.name_lens(context.path, facts, selections, type_name, &mut members);
self.duplicates.extend(duplicates);
Expand All @@ -310,6 +318,11 @@ impl<'a> Readers<'a> {
let connection = connection.map(|connection| {
self.connection_members(connection, type_name, &members, facts.nodes, context)
});
let paged = paged.map(|paged| PagedMembers {
container_type: type_name.to_string(),
page_info_type: paged.page_info_type.clone(),
page: paged.page.clone(),
});
let satisfied =
bubbles.then(|| satisfied(type_name, type_is_abstract, &members, self.naming));
let mut field_errors = context.scans_errors().then(|| {
Expand Down Expand Up @@ -367,6 +380,7 @@ impl<'a> Readers<'a> {
&child.selections,
child_context,
child.connection.as_ref(),
child.paged.as_ref(),
false,
child.bubbles,
)
Expand All @@ -378,6 +392,7 @@ impl<'a> Readers<'a> {
accessors,
refetch,
connection,
paged,
satisfied,
reports_missing: false,
field_errors,
Expand Down Expand Up @@ -520,6 +535,12 @@ impl<'a> Readers<'a> {
scope.declare(name, Kind::Instance, format!("the connection's `{name}`"));
}
}
if facts.paged {
scope.declare("paged", Kind::Static, "the paged list's slots");
for name in ["hasNext", "isLoadingNext", "loadNext"] {
scope.declare(name, Kind::Instance, format!("the paged list's `{name}`"));
}
}
for member in members.iter_mut() {
if let Some((name, what)) = written_accessor(&member.selection) {
scope.declare_written(&name, Kind::Instance, what, written_key(&member.selection));
Expand Down Expand Up @@ -718,6 +739,7 @@ impl<'a> Readers<'a> {
is_abstract: lens_abstract,
selections: child.clone(),
connection: None,
paged: None,
bubbles: *bubbles,
within_catch: catch.is_some(),
response_key: None,
Expand All @@ -739,6 +761,7 @@ impl<'a> Readers<'a> {
is_abstract: lens_abstract,
selections: child.clone(),
connection: None,
paged: None,
bubbles: false,
within_catch: false,
response_key: None,
Expand Down Expand Up @@ -1008,6 +1031,7 @@ fn linked_read(
is_abstract,
storage_key,
connection,
paged,
required,
catch,
bubbles,
Expand Down Expand Up @@ -1051,6 +1075,7 @@ fn linked_read(
is_abstract: *is_abstract,
selections: child.clone(),
connection: connection.clone(),
paged: paged.clone(),
bubbles: *bubbles,
within_catch: catch.is_some(),
response_key: Some(alias.clone().unwrap_or_else(|| name.clone())),
Expand Down
7 changes: 5 additions & 2 deletions compiler/src/directives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ use crate::documents::Marker;

/// The directives each place takes. A query also states its cache
/// expiration, the first directive that is not Relay's
/// (`docs/decisions/an-operation-states-its-expiration.md`), and a mutation
/// (`docs/decisions/an-operation-states-its-expiration.md`), a mutation
/// names the records it deletes, the second
/// (`docs/decisions/a-mutation-names-the-record-it-deletes.md`).
/// (`docs/decisions/a-mutation-names-the-record-it-deletes.md`), and a list
/// field says it is paged by number, the third
/// (`docs/decisions/a-paged-list-merges-its-pages-by-number.md`).
const QUERY: &[&str] = &["throwOnFieldError", "cacheExpiration"];
const MUTATION: &[&str] = &["throwOnFieldError", "deletes"];
const SUBSCRIPTION: &[&str] = &["throwOnFieldError"];
Expand All @@ -43,6 +45,7 @@ const FIELD: &[&str] = &[
"deleteRecord",
"stream",
"stream_connection",
"paged",
];
const INLINE_FRAGMENT: &[&str] = &["include", "skip", "defer", "alias", "catch"];
const SPREAD: &[&str] = &["include", "skip", "defer", "arguments", "alias", "catch"];
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const HEADER: &str =
/// of its format, so code of another format fails to compile at that line
/// and the marker says which side is behind. A change to what generated
/// code names in the runtime raises it, here and in the runtime together.
pub const FORMAT: u32 = 20;
pub const FORMAT: u32 = 21;

/// The Swift target: Swift's names, which the program is decided in, and
/// the printers that write the decided program.
Expand Down
Loading
Loading