Summary#
A contract is an ordinary public interface in a versioned package — IMailSender in Osysharp.Mail. An
adapter is a class in another package that implements it — ResendSender in Osysharp.Mail.Resend, which writes
use Osysharp.Mail@0;. The app picks an implementation in a setting (app.Mail = new MailSetup { Sender = new ResendSender { … } };). Nothing here is special syntax: it is interfaces, packages and app settings, and the version
is what keeps the three honest.
// In a contract kit: what carries a notification.
public interface INotifier {
string Key { get; }
string Notify(string to, string text);
// Added in a MINOR version, with a body — so every notifier written before it still compiles, and says this.
string Describe() => "the '" + Key + "' notifier";
}
// In an adapter kit, written against the version before `Describe` existed — unchanged.
public class PagerNotifier : INotifier {
public string Key { get; } = "pager";
public string Notify(string to, string text) { return "paged " + to; }
}
string Which() { INotifier n = new PagerNotifier(); return n.Describe(); } // "the 'pager' notifier"Signature#
package Acme.Notify { version "0.2.0"; model "model/*.osy"; } // public interface INotifier { … }
package Acme.Notify.Pager { version "0.1.0"; use Acme.Notify@0; model "…"; } // class PagerNotifier : INotifier
app.Notify = new NotifySetup { Notifier = new PagerNotifier { … } }; // the app's choiceDescription#
Which changes need a major version?#
For a CALLER of a package, adding a member is harmless. For an IMPLEMENTOR it is the breaking change: an adapter written against the previous version does not declare the new member. So a contract's version answers to its implementers:
| change to a public interface | breaks an implementor? | version |
|---|---|---|
| a member added with a default body | no — it inherits the default | minor |
| a default's body changed, or a default added to an existing member | no | minor (or patch) |
| a new contract | no — nothing implements it yet | minor |
| a member added without a default | yes — every implementor lacks it | major |
| a member removed, or its parameters or return type changed | yes | major |
| a member's default removed | yes — implementors that relied on it lack a body | major |
| the contract removed, or made non-public | yes | major |
The members counted include those an interface inherits from another interface of the same package
(IWritable : IReadable) — an implementor owes those too.
Where is it checked?#
At osy lock, which is where a new copy of a package is accepted into a project: the copy the lock pinned before
is compared with the new one, and a change that breaks implementers under the same major is refused, naming the
contract, the member and the rule:
CONTRACT_BREAKS_IMPLEMENTERS: Osysharp.Mail 0.2.0 breaks every implementor of its contract: 'IMailSender.Region'
was added with no default (`string Region()`). An adapter built against Osysharp.Mail 0.1.0 would stop compiling,
so this is a MAJOR change — make it 1.0.0, or give `Region` a default body in the interface — which every existing
implementor then inherits, and which is a minor change.Every verb that resolves a RELEASE goes through the same check — osy lock, osy update, and the pin osy compile
and a deploy record for a use not pinned yet — so no verb walks around it. A previous copy that is no longer in the
local package cache cannot be compared, and is not treated as a break. A package in your own workspace is read from its
source and never pinned, so it has no previous copy to compare with here; its release is compared at osy publish.
At osy publish, the release being created is compared with the newest one already published, by the same rule
and before any other surface check.
What does an adapter that no longer fits see?#
An implementation missing a member is refused at compile, naming the contract, the member's shape, and — for an adapter from another package — the version it was built against and the version this app composes:
'Osysharp.Mail.Resend.ResendSender' implements 'Osysharp.Mail.IMailSender', which requires 'Region' with no default
— and 'Osysharp.Mail.Resend.ResendSender' does not declare it. Add `public string Region() { … }` (no `override`:
a contract member is implemented, not overridden). 'ResendSender' comes from Osysharp.Mail.Resend 0.1.0, built
against `Osysharp.Mail@0`, and 'IMailSender' is Osysharp.Mail 0.2.0's. …When the adapter's constraint does not admit the contract's version at all, it says so and names the major to use:
… built against Osysharp.Mail@0 — and this app composes Osysharp.Mail 1.0.0, outside that constraint … Use a version of Osysharp.Mail.Resend built for Osysharp.Mail@1, or depend on Osysharp.Mail@0.
How do I find the contracts and their implementations?#
osy kits lists every contract in scope with every implementation, and what this app wired in each setting;
osy model --json carries the same as contracts and contractWiring (Finding a kit, and learning one from its tests).
Can a contract keep a member for its own package to call?#
Yes: declare the member internal. Any package may implement it, and only the contract's package may call it — so
every call goes through that package's own public function (internal — a member only its own package may call).
See also#
- internal — a member only its own package may call — a contract member only its own package may call
- Interfaces — a contract several types can satisfy — interfaces and default members
- package.osy —
package.osy,use, publishing - Finding a kit, and learning one from its tests —
osy kits - The Mail kit — send a message, keep a record, pick a sender —
Osysharp.Mail, a contract kit