These principles guide the design of podcast-clj, its API and its code.
Architecture explains how each part is built.
- A library, not a framework. Pure functions over plain Clojure data.
How to store data, play media and draw the UI is up to you.
- Everything a podcast app needs. A feature is left out only when it
makes no sense in a library, like audio processing, or when it would be
an immense amount of work.
- Standards first. Each spec is implemented as written. New features
are built on what the specs already offer, including the parts that most
apps ignore, and podcast-clj only invents where the specs leave a gap.
- Lenient, but transparent. Real feeds break the specs in many small
ways, and an app has to show them anyway. Each workaround is a named
quirk that can be switched off for strict parsing, and every repair goes
in a report. Nothing is dropped silently: unknown elements are kept, so
writing a feed back keeps what was read, and the report names anything
that couldn't be kept.
- Sync without a server. The listener's profile is a CRDT, so devices
merge their changes in any order, and the later of two changes wins. Any
place that can hold files is enough to sync through, even a plain folder.
- One codebase. Almost everything is
.cljc and runs on the JVM, in
Node and in the browser. Platform code lives in small adapters, and in
ClojureScript a function that sends requests returns a promise. - Tested on real feeds. The tests run against a corpus of real feeds,
not only hand-written fixtures.
- Compose, don't reimplement. The high-level functions are built from
the lower-level ones, so you can start with one call and drop down a
level when you need more control.
- Convention over configuration. A function requires only what it
can't work without. Everything else has a default, and the options are
a map that comes last and can be left out, which a test checks.
- The feed model is data. One tag registry describes every element.
The parser, the emitter, the validator, the Malli schemas and the
reference docs all read it, and an app can extend it with its own
elements.
- The app's state is one map. The subscriptions, feeds, queue and play
progress of an app are kept in one map that fits in an atom, and each
change returns the new map along with what it did.
- Pure unless marked. Only functions that end in
! do I/O. A function
that needs the time takes it, except those that end in ! and the
high-level ones, which read a clock that a test can fix. Requests are
plain maps built and read by pure functions, so any HTTP client can send
them. - Errors are data. Functions that do I/O return their failures as data
instead of throwing, so one failed request doesn't lose the rest. Only
the low-level functions that send a single request throw.
- Services are defaults. What podcast-clj knows about a service, such
as its address or limits, is a default that an option can replace. An
app shouldn't need a new release of podcast-clj when a service moves or
closes.
- Light on the network. Feeds are polled with conditional requests and
media files are read with byte ranges, so podcast-clj fetches no more
than it needs.
- Safe by default. Every fetch has limits of size and time, URLs from
untrusted sources are only fetched if they're on the public internet,
and secrets don't print.
- Layers without cycles. A namespace only requires namespaces in its
own layer or below, and the general tools at
the bottom know nothing about podcasts. A test enforces it.
- Cite the spec. Where code implements a standard, a short comment or
the docstring names the spec and its section.