5 ms·
Taxi: A language for documenting data models and the contracts of APIs
- gravypod 4y agoAre there any benefits of Taxi over Protobuf (the language)?
- eyelidlessness 4y agoFrom the looks of it, documentation as a first class part of the schema definition, and enforceable, arbitrarily meaningful subtypes of scalars. There are tons of use cases for those features, which are achievable with Protobuf for struct-y types, but the advantage of using scalar subtypes is better integration into common host systems without gymnastics to wrap/unwrap the values.
- martypitt 4y agoExactly. Also, the idea that those types can be embedded across multiple schemas is how you can describe cross-system interoperability, without requiring all systems to have the same Model for "Customer" or "Account"
- Groxx 4y agoProtobuf is rather (extremely?) weak type-wise, so a lot of it probably boils down to "it has reasonable / usable types". Like, it doesn't even have semantics for saying "this string is assumed to be a timestamp" in a way you can refer to in multiple locations, the best you can do is name your field "ThingTimestamp" and hope people get the right idea. This also seems to be describing a querying system, data mapping (e.g. for using csv data), runtime calculated data (likely used to drive the csv reading stuff), etc a number of other things that are less related to just data and more how to get that data, beyond just an RPC API described by types. E.g. the annotations and computed types could describe a fair number of sql queries in your data model, so you could probably just point it at a database with very minor "glue" code. Whether that second part is relevant probably depends on how much you want to write in your implementation language vs in taxi's language... but it's substantially different than protobuf in any case.
- gabereiser 4y agoUmmm... google.protobuf.Timestamp is a thing. Use it. Don't just throw an int32 at me and _say_ it's a timestamp. /s
- Groxx 4y agoIt's a struct which contains an int field, which iirc has more encoding, transfer, and decoding costs than an int (nobody cares when it's one, you might care when it's many thousands), and implies nullability because all message types are nullable. And unless you're using a Google-blessed code generator in a Google-blessed language, it's also noticeably more verbose to use[1]. But yes, you do indeed have user-defined structure (message) types in protobuf, and you can bend them quite far. There are also annotations of a sort, though support there seems worse, personally I've only very rarely been able to use them cross-language at all. [1] yeah, there are alternate code generators, but their support and quality is "mixed" to put it mildly. They do exist though, and many have a plugin system of some kind for making your own less-painful custom types.
- martypitt 4y agoTaxi is a documentation and query language, but not a runtime wire format. ie., you can't encode a message into Taxi and send it across the wire, like you can with Protobuf. Taxi is intended to work with protobuf, rather than replace it. The idea is teams work on the Taxi type system collaboratively, building a library of terms (FirstName / LastName / etc), which can be applied consistently across multiple team's schemas. Developers using Protobuf / OpenAPI / RAML / etc., can embed those terms directly into their existing schemas, to improve documentation, and describe interoperability between multple internal systems. (TeamA's field called "givenName" is the same data as TeamB's field called "FIRST_NAME") As I write this, I realize our docs are missing how to use Taxi with Protobuf, which is kinda embarrassing. There's examples documented elsewhere[0], but we need to get the Taxi docs updated! [0] https://docs.vyne.co/reference/message-formats/protobuf/#embedding-semantic-types-in-protobuf https://docs.vyne.co/reference/message-formats/protobuf/#emb...
- 4y ago
- cturtle 4y agoNot to be confused with Taxi [0], the esolang where programs are a series of directions for a taxi. "Programs are built by giving directions to destinations on a map which is where computation takes place." [0]: https://esolangs.org/wiki/Taxi https://esolangs.org/wiki/Taxi
- jensenbox 4y agoWhat benefits does this provide over Swagger/OpenAPI? The ecosystem for OpenAPI is pretty rich - what is the intent here? Does it live with OpenAPI or replace it?
- martypitt 4y agoOriginally, Taxi was built because the readability / signal-to-noise ration of OpenAPI is pretty poor. Plus, we wanted to build a documentation spec that could automate integration - describe the specific contracts of what an API does, the data it exposes, and how and where that data can be leveraged in other APIs. You can't really read a swagger spec to get a sense of what it can do - you have to rely on tooling and UIs to make it understandable. For a toolkit who's entire purpose is documentation, that felt pretty frustrating. Also, in OpenAPI, you can't document how data attributes interact - ie., the specifics about what data an operation needs (beyond a map of Strings and Numbers). We wanted a way of saying "This operation accepts an Email Address, and returns an AccountBalance" that was machine understandable. However, you're right - the OpenAPI ecosystem is really rich, and we're not going to displace it. We've built Taxi extensions for OpenAPI[0] which allow you to embed the taxi type metadata directly inside OpenAPI specs. This helps with describing the interoperability (and is how tooling like Vyne[1] can automate orchestration between unrelated APIs) There's Swagger-to-taxi[2] tooling which will convert a Taxi schema to Swagger and back again - though this is more used internally to transpile OpenAPI on the fly. I doubt anyone will throw out their OpenAPI specs for Taxi - and that's ok. [0] https://docs.taxilang.org/generating-taxi-from-source/#swagger--openapi https://docs.taxilang.org/generating-taxi-from-source/#swagg... [1] https://vyne.co https://vyne.co [2] https://gitlab.com/taxi-lang/taxi-lang/-/tree/develop/swagger2taxi https://gitlab.com/taxi-lang/taxi-lang/-/tree/develop/swagge...
- martypitt 4y agoHey folks. Creator of Taxi here. Pretty chuffed to wake up and find Taxi on the front page of HN. We built Taxi originally as a "Better OpenAPI", being frustrated with the signal-to-noise ratio of OpenAPI / Swagger's format. However, over the years, we've found it shines when used to describe how data and APIs from different teams & systems can interoperate. Rather than describing an API endpoint as accepting a "String", it's much more descriptive to say "EmailAddress", or "WorkEmailAddress". By using these same scalar types to enrich models in other systems, you can start building pretty powerful knowledge graphs of how everything in your org hangs together. Happy to answer any questions.
- rzazueta 4y agohttps://xkcd.com/927/ https://xkcd.com/927/ Edit: Sorry, that was probably not polite. But I've been here before too many times. IO Docs, RAML, Blueprint... I guess the question is - why should I adopt this if I'm already committed to the Open API ecosystem, and why should I consider it if I am still deciding?
- martypitt 4y agoWe started trying to be a better OpenAPI, however the goals have shifted, and today we're trying to build a cross-team semantic language for describing data and APIs - one which describes how data and APIs from independent teams can interact. OpenAPI is a great way for someone to publish their API, so that another person can discover it, generate a Client SDK, and start building integrations. Taxi has the same goal, but for machines - so that software can automatically discover another API, and understand semantically how to interact with it. The two techs can co-exist quite happily, and you get great documentation benefits in embedding Taxi types in your OpenAPI spec [0] IMO, OpenAPI has a poor signal-to-noise ratio - a documentation tool you can't actually read without tooling. That feels misaligned to me. More importantly, OpenAPI doesn't do a good enough job in describing the semantics of a contract. What String should I pass to your API? EmailAddress or CustomerId? In this transaction, where do I place the BuyerCustomerId vs SellerCustomerId? That's an important distinction, which other tools aren't trying to address. With Taxi, we want to document APIs and Data in a way that tooling can understand across the organsiation, without having to fall back to reading docs, and that doesn't require shared models across the org. From there, we can build tooling can understand the semantics of an API, and then automate the integration (That's our other tool, Vyne). [0] https://docs.taxilang.org/generating-taxi-from-source/#examples https://docs.taxilang.org/generating-taxi-from-source/#examp...
- kgeist 4y agoAt work I often use Go notation for quickly describing my models to share with other devs, or for collaboratively iterating on ideas; both in text form and on UML diagrams. The syntax is pretty simple and doesn't have "noise", it supports type aliases, too ("type Email string"), you can emulate optionals, there are useful built-in containers such as slices and maps on the language level (no need to import additional packages), most IDEs support syntax highlighting, etc. It's an ad hoc solution and there's no tooling for it but Go has a built-in parser for its syntax so technically it's not hard to write a codegen tool based on that.
- beders 4y agoDefining basic scalars and giving them good names is useful. Trying to define models (as Taxi defines them) statically is often a source of hidden technical debt as models change over time and different teams in different parts of an org can't agree on what a model should contain. I like that the creator of Taxi is already aware of the anti-pattern lurking behind common domain modeling but doesn't take those lessons learned far enough. Case in point: the field names in models themselves should be the actual Types (again, in the nomenclature of Taxi) `firstName: FirstName` is redundant. It is equivalent to an alias. Instead a model should just be a open set of Types (I would probably call them attributes). That's what Clojure Spec gets right from the get go and leads to much more flexible and open systems. Given a model Person with something like this: firstName: FirstName middleName: MiddleName lastName: LastName ssn: SSN If I only have a FirstName and LastName and don't know the ssn yet, I can't pass these around without having to define an extra model `PersonButWithoutSSN`. I can try to mark ssn as optional, but aren't all these fields optional? And in what combination? Lastly I would advise against enforcing business logic in models, which is what the conditional blocks are effectively doing. Even if just used for data transformation: You'll run the danger of embedding a full scripting language (and you are pretty far already in that direction) Stick to data coercion on the Type level. Any other transformation should be out of scope or defined separately to maximize interop.
- martypitt 4y ago> Trying to define models (as Taxi defines them) statically is often a source of hidden technical debt as models change over time and different teams in different parts of an org can't agree on what a model should contain. I completely agree. My view (and the view of Taxi) is that Types should be shared by teams, but never models - for exactly the reasons you've outlined. Systems need a way of describing their data as a composite of those scalar types - that's what Models provide. Field names are part of that contract, as they tell you structurally where information is. Types describe semantically what that information contains - you need both to semantically document the contract of data. However, if multiple teams start sharing models, you end up with exactly the `PersonButWithoutSSN` situation you've described, which is a BadThing. Our vision is to let data consumers to semantically describe the data they require, in the format they want it. If that doesn't align with how a producer is emitting it, or requires stitching data from multiple producers, that's OK - tooling can enable that, provided you have rich semantic schemas.
- _8j50 4y agoNot to be confused with the abominable Taxii
- wiz21c 4y agoLooks nice. But miss this : https://en.wikipedia.org/wiki/Object_Constraint_Language https://en.wikipedia.org/wiki/Object_Constraint_Language That little language really helps to express constraints over data.
- jitl 4y agoI’m sad to see that Taxi doesn’t support union/sum types! We make use of them quite heavily at Notion - our “block” model is a union of 60 subtypes - so it’s a make or break feature for any semantic language we’d use internally. For Kotlin, taxi could support this by generating sealed (data) class. GraphQL’s schema language seems similar in some respects, and supports union types but not generics. I’m also curious about how teams or systems onboard to Taxi. There’s a lot of advice about “models shouldn’t be shared between teams” and such. How would you suggest an engineering org go from a shared set of models defined in some other language today, to model-per-team defined in Taxi? I’m a bit confused about to what degree models should encode some existing types in an implementation, or if models should be idealistic, and the implementation will need to do a lot of data munging to produce a structure that matches the model. Finally, how should teams think about computed properties / expressions? If models describe the input or output of an API my team owns, wouldn’t our API compute these properties internally? Why expose a possibly-incorrect expression to compute it?
- martypitt 4y agoHey. There's a few things here, and I'd be keen to chat about them in more detail - I'm marty at vyne dot co, or often on our slack - if you're keen to dive into anything further. > I’m sad to see that Taxi doesn’t support union/sum types! These are in progress right now. I'm not sure how well it fits your usecase, so it'd be interesting to chat. > For Kotlin, taxi could support this by generating sealed (data) class. That's a nice idea. We have a kotlin generator for taxi, but I hadn't gotten to how we would handle the union types. I feel like you just saved us a few iterations! :) > I’m also curious about how teams or systems onboard to Taxi. There’s a lot of advice about “models shouldn’t be shared between teams” and such. What we're advocating against here is a common domain model - where everyone in the org is expected to model a Customer in exactly the same way - this leads to everything being optional. In terms of onboarding, there's normally a few components: * A shared set of types (no models), describing in a taxi project, which lives in a git repo somewhere * Teams start describing their APIs using those types - either by publishing a Taxi model directly, or (more commonly), embedding the taxi types in their Swagger / Proto etc. * Teams then start querying using TaxiQL and a query server (like Vyne) to fetch data from across those APIs. This keeps the consumers decoupled from the producer schemas. > Finally, how should teams think about computed properties / expressions? [..] wouldn’t our API compute these properties internally? Yeah, it's uncommon for a producer contract to include a computed property - though we have seen teams do this against things like CSV models. It's more common for consumers to use these for declaring the computed result they want for a field in a query, and let the query service handle the computation. This allows either ad-hoc computation (defined in a field), or shared common expressions, defined as an expression type.
- BiteCode_dev 4y agoI wish CUElang is going to succeed. From what I'm seeing, it's already better than Taxi at what Taxi is supposed to be excellent at, and yet does much more, while still not being Turing complete. I enjoy it better than JSON, YAML, TOML, and any respective schema associated to those. Sum and Union types are a killer feature, espacially since CUE can define a type and a value using the same syntax. But it must be hard to implement, because for now, only the reference implementation, in Go, exists.
- odipar 4y agoI also personally think CUE hits a sweet spot. The union (pun intended) between types and values (both sets) is really a game changer! CUE is indeed not trivial to implement, especially its intricate semantics are tricky. I work an a CUE alternative, called ReSeT to really understand the semantics better. See: https://old.reddit.com/r/ProgrammingLanguages/comments/uukxvp/reset_set_based_programming_language/ https://old.reddit.com/r/ProgrammingLanguages/comments/uukxv...
- prionassembly 4y agoI've wanted to use cuelang twice already, but having to install the go language on Ubuntu is a major inconvenience. Instead I used NestedText (I tend to do all my parsing with Pydantic anyway).
- gavinray 4y agoThis feels almost identical to GraphQL With GraphQL, you'd define types -- say something like: type User { id: ID name: String age: Int todos: Todo[] } type Todo { id: ID user_id: ID text: String is_completed: Boolean } These can be autogenerated based on, for instance, the tables in your SQL database Then you can use tools to generate query functions, like: query { users(where: { age: { _lte: 25 } }) { name todos(where: { is_completed: { _eq: false } }) { text } } } And finally generate a typed SDK so that operations are strongly-typed for whatever language it is you're using to query the GraphQL API