Specification schema for models, data types, and domain-interfacing operations that facilitate Documentation Driven Development for self-generated-code, self-validating, self-describing, auto-tested. and self-testing generated source code scaffolding for the Domain-Interfacing logic.
Traditionally, a field's specification is broad, abstract, or meaningless which leads to writing documentation that attempts to provide meaning to the consumer of the code. We know what an Integer, Float, Byte, String, etc. are - but these are just labels we apply to varying ways of storing and manipulating data. If the author provides sufficient information regarding the acceptable values, ranges, and edge case - there is still the matter of writing a slew of tests to ensure the fields lie within the specifications. Not to mention, hoping the documentation remains in line with the implementation as teams, features, and requirements change and grow.
While still using the same underlying storage and manipulations considered familiar across the landscape, fields in DeepThought attempt to remove the tediousness, prevent stale documentation, as well as describing what is. Each field specifies a code-friendly reference name, a human-readable meaningful name, description, constraints, coercive manipulations of the underlying data type - plus units if applicable.
Assume you need a field that represents the height of something, maybe a person:
Traditionally, this would be a field of type int
- if you're lucky or forward thinking, the variable name might be
heightFt
- more often than not it's named height
.
With DeepThought, this field could be specified as:
# person.fields.yml
id: height
type: number
name: Height
description: How tall the person is
unit: foot
coerce: True
constraints:
- minimum: 0
- maximum: 8
- precision:
value: 1
unit: inch
Or, for the rest of the world:
# person.fields.yml
id:
type: number
name: Height
description: How tall the person is
unit: meter
coerce: True
constraints:
- minimum: 0
- maximum: 3
- precision: 0.01
You tell DeepThought the unit to present to the Domain-Interfacing Use Case.
The constraints placed on a field, as seen above, can be expressed with inferred or explicit units. Constraints apply to
a set of underlying types: minimum
is not a meaningful constraint to a string
or byte
.
TODO
DeepThought's extensible unit system comes bundled with commonly used units. Through the use of dimensional fields, DeepThought will also test and validate correctness through dimensional analysis. Of course, not every field has a need for units - unit-less scalars are just as valid.
To define custom units, provide a key that can be used in fields to refer to and a few other basic pieces of information:
# units.yml
id: fathom
name: Fathom
description: A length measure usually referring to a depth.
label:
single: Fathom
plural: Fathoms
symbol: fm
conversion:
unit: meter
factor: 1.8288
While any unit can be specified in the conversion attribute, specifying the conversion factor in relation to International System Units (SI), allows for wider, easier adoption in more systems and projects.
To specify an entirely new dimensional unit, simply omit the conversion attribute:
# units.yml
id: click
name: Click
description: The click count of something.
label:
single: Click
plural: Clicks
symbol: clicks
id: slam # This may be meaningful to someone...
name: Slams
description: Count of something clicked being broken.
label:
single: Slam
plural: Slams
symbol: slams
conversion:
unit: click
factor: .001 # If clicked 1000 times, must be broken
DeepThought also supports defining multi-dimensional units, or compound units, even complex units.
For example, velocity is distance over time, acceleration is the change in velocity over time:
id: velocity
name: Velocity
description: The change in physical position per unit time.
label:
single: meter per second
plural: meters per second
symbom: m/s
unit:
unit: meter
dividedBy:
unit: second
id: acceleration
name: Acceleration
description: The rate of change in velocity per unit time.
label:
single: meter per second^2
plural: meters per second^2
symbol: m/s^2
unit:
unit: meter
dividedBy:
unit: second
exponent: 2
- bps, Bps, kbps, kBps, mbps, mBps
- bit, byte, kilobit, kilobyte, megabit, megabyte, gigabyte
- ampere
- Couloumb, joule
- acceleration, velocity, force, presssure
- foot, inch, mile, yard
- centimeter, millimeter, meter, kilometer
- ounce, pound, gram, kilogram, microgram
- second, minute, hour
- hertz
- Elementary Charge
- Gravitational Constant
- Lightspeed
- Planck
Models are the data structures that are directly consumed, or returned by, Domain-Interfacing Use Cases. Models comprise the necessary Fields to fully describe that which the domain can consume, manipulate, transform, parse, or return.
Assume, you had a use case for calculating the volumetric density of a can of Pringles and created the following models:
# pringles.fields.yml
- id: radius
type: number
name: Radius
description: Distance from edge to center of a Pringles can
unit: centimeter
coerce: True
constraints:
- minimum: 0
- maximum:
value: 1
unit: meter
- id: height
type: float
name: Height
description: Distance from bottom to top of a Pringles can
unit: inches
coerce: True
constraints:
- exclusiveMinimum: 0
- id: mass
type: number
name: Mass
description: The amount of matter contained in a Pringles can
unit: grams
coerce: True
- id: density
type: number
name: Pringles Can Density
description: The mean density of the a Pringles can
unit: si_density
# pringles.models.yml
pringles_can_dimensions:
name: Pringles Can Dimensions
description: The physical measurements of a can of Pringles
fields:
- radius
- mass
- height
pringles_can_density:
fields: density
When specifying Domain-Interfacing Use Cases, Deepthought has no knowledge of your implementation. Be it a single method, a chain of methods - that is up to you. Specify the input Model, and the output Model - knowing you can trust that your input is valid, tested, and in the exact form that is expected. If dimensionally correct, DeepThought will generate the scaffolded code for you to implement - if incorrect, you will know before you even start coding.
Correct:
# pringles.usecases.yml
calculate_can_density:
name: Calculate Density of Cylinder
description: Calculates the density of a cylinder
input: pringles_can_dimensions
output: pringles_can_density
Incorrect:
# pringles.fields.yml
pringles_can_density:
name: Pringles Can Density
description: The mean density of the a Pringles can
unit: meter / second
# pringles.usecases.yml
calculate_can_density:
name: Calculate Density of Cylinder
description: Calculates the density of a cylinder
input: pringles_can_dimensions
output: pringles_can_density
There is no way that the input units (inches
/centimeter
and grams
) - could possibly be yield to the expected
units (meter / second
) specified in the density
model based on the known units in the system.
Use Cases can be chained, accept multiple models, provide permission validation and are completely reusable.
Most architectural philosophies preach of the "ATOMIC!": Atomic commits, atomic classes, atomic use cases. It makes sense to reuse Use Cases, and put them together like Legos or microservices. Using the Use Case schema, you can specify the next use case in the chain. Inputs can be trusted, and output models must be dimensionally possible from the input models for the code-generated scaffolding to work.
# usecases.yml
calculate_cylinder_density:
name: Calculate the Density of a Cylinder
description: Calculates the density of a cylinder
input: pringles_can_dimensions
output: pringles_can_density
next: calculate_time_to_empty
calculate_time_to_empty:
name: Calculate the time to empty a can of Pringles
description: Calculates the time it will take to consume an entire Pringles can. Once You Pop, You Can't Stop!
input:
- pringles_can_dimensions
- density_consumption_rate
output: time_to_empty_can
next: extrapolate_emptying_pantry
extrapolate_emptying_pantry:
name: Calculate the time to empty the pantry
description: Extrapolates the time it takes to empty a can of Pringles to approximate the time to devour the pantry full of Pringles.
input:
- time_to_empty_can
- pringles_can_density
- pantry_dimensions
output: time_to_obesity
Hard-coding names and descriptions, as above, doesn't facilitate portability or re-usability. To rectify Units, Field, Data Types can utilize template replacers of the containing structures.
# fields.yml
radius:
type: float
name: Radius
description: Radial dimension of {{ model.name }}
unit: centimeter
coerce: True
height:
type: float
name: Height
description: Top to bottom dimension of {{ model.name }}
unit: inches
coerce: True
mass:
type: float
name: Mass
description: Physical quantity of matter in {{ model.usecase.focus }}
unit: grams
coerce: True
density:
type: float
name: Density of {{ model.usecase.focus }}
description: The mean density of {{ model.usecase.focus }}
unit: kilogram / (meter ^ 3)
# models.yml
dimensions:
name: Dimensions of {{ usecase.focus }}
description: The physical measurements of {{ usecase.focus }}
fields:
- radius
- mass
- height
# usecases.yml
get_cylinder_density:
name: Get Density of {{ this.focus }}
description: Calculates mean density of {{ this.focus }}
input: dimensions
output: density
get_pringles_can_density:
extends: get_cylinder_density
focus: Pringles Can
next: calculate_time_to_empty
TODO
I have always been drawn to software architectural design. I also am one of those blatantly guilty of focusing on code, not the documentation - and if you know me, I'm lazy. I don't write a lot of APIs, which seems to be the focus of GraphQL and Swagger - but the idea of schema-first, validated, self-describing... resonated with me. Take Ansible, with well-defined YAML formatted documentation, the ArgSpec must still be implemented; allowing for divergence in implementation and documentation.
The goal here is to create a specification that is simple, scalable and consistent enough for non-developers to maintain. A specification that can drive and generate human-readable documentation (Swagger). Automatically generate descriptive and meaningful classes in almost any language (GraphQL). Testing is a natural consequence of the specification and constraints therein (TTD).
Is this complete? Not by a long shot. If you see the benefit, join me! Because, for me, this will completely change how I approach software development.