Osy#betaRun it on Osyrin: osyrin.com

Reference / Project

Contracts between kits — an interface in a package, and its version

package A { version "0.2.0"; } // declares public interface IThing { … } package A.Impl { use A@0; } // implements it class Impl : IThing { … } app.Setting = new Setup { Thing = new Impl { … } };

A kit that needs something done declares an ordinary public interface; another kit implements it; the app picks one in a setting. A member added with a default is a minor version, a change that breaks an implementor is a major one, and `osy lock` and `osy publish` refuse it under anything less.

stable1 example compiled by CIprojectpackageskitscontracts

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 choice

Description#

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 interfacebreaks an implementor?version
a member added with a default bodyno — it inherits the defaultminor
a default's body changed, or a default added to an existing membernominor (or patch)
a new contractno — nothing implements it yetminor
a member added without a defaultyes — every implementor lacks itmajor
a member removed, or its parameters or return type changedyesmajor
a member's default removedyes — implementors that relied on it lack a bodymajor
the contract removed, or made non-publicyesmajor

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#

Related

Interfaces — a contract several types can satisfy

An `interface` declares what a type must do without saying how — methods and property contracts, and no state. A member…

package.osy

The manifest that makes a git repository a publishable Osy# package. It names the package, says which of the…

Finding a kit, and learning one from its tests

Lists the kits (packages) this project can build on — each one's contracts, the kits that implement them, and what this…

The Mail kit — send a message, keep a record, pick a sender

`Osysharp.Mail` is the contract between an app that sends mail and whatever carries it. `SendMail` writes the message…

settings a package asks its app for (app settings)

A package — or an app — declares a setting by marking a class `[AppSetting("Name")]`. The app that uses it fills it in…

internal — a member only its own package may call

A function, a class member or a contract member declared internal is callable only from the package that declares it…