Better Laziness Through Hypermedia -- Designing a Hypermedia Client

26
BETTER LAZINESS THROUGH HYPERMEDIA PETE GAMACHE // 7 APRIL 2014 // WS-REST @ WWW2014

Transcript of Better Laziness Through Hypermedia -- Designing a Hypermedia Client

B E T T E R L A Z I N E S S T H R O U G H H Y P E R M E D I A

P E T E G A M A C H E / / 7 A P R I L 2 0 1 4 / / W S - R E S T @ W W W 2 0 1 4

I M A K E A P I S

• APIs are great

• Separation of concerns is fantastic

• Service oriented architecture is wonderful

• An ecosystem of interoperable APIs would be so nice (coming soon)

P R O B L E M

• People need to consume these APIs somehow

• Solution 1: Users write their own client layer

• Not too hard for a ca. 2007 JSON API

• Solution 2: I make API client software

• Users get it for free

I U P D AT E A P I S

• API versioning keeps the worst of change-related problems away

• Breaking changes can be avoided with care

P R O B L E M

• Users need to get those updates

• Solution 1: Users update the client layer they wrote

• Solution 2: I update API client software

B I G P R O B L E M

• Someone needs to write and maintain the client layer

• If that’s me, I need to push updates to my users

• Users need to retest their software with the new client library

• Users need to deploy the new client software

E F F O R T A N D L A Z I N E S S

• Part of good software development is minimizing effort and maximizing output

• Laziness is the quest for the sweet spot

• DRY, YAGNI, SAAS, packaging work toward this goal

• Effort is finite; spending it on uninteresting work is inadvisable

W H AT M A K E S G O O D S O F T W A R E ?

• Laziest way to access whatever functionality it affords

• Complete, internally consistent abstraction

• Few unnecessary demands on the end user

B A C K T O T H E A P I U P D AT E P R O B L E M

• Solution to the update problem: Hypermedia APIs

• API publishes its entire functionality through named hyperlinks

• These hyperlinks drive all API interaction (HATEOAS)

• Generic client software can automate many parts of this, given a standardized hypermedia format

W H Y D O E S N O O N E D O T H AT ?

• Slight burden on API designer

• Low degree of standardization among hypermedia formats

• Very few industry examples or sample programs

• Existing hypermedia client software fails the end user.

W H AT ’ S W R O N G W I T H C L I E N T S

• Designed for humans, not machines

• Thin abstraction over HTTP

• Mingling of procedure and intent

• Few ways to extend incoming data with local code

• Not substantially easier than DIY

G O A L S F O R A G E N E R I C A P I C L I E N T

• End user code expresses intent, not procedure

• Client uses idioms of its native platform

• Usable with almost no configuration

• Allows extension of incoming data

• Does not impose philosophical burden on users

R E S T V S . H Y P E R M E D I A

• REST is good, REST is great, all hail REST

• REST is not a good match for every API or deployment

• A client that doesn’t assume or impose design philosophy is strictly more useful than one that does

• Fewer requirements —> Greater adoption

A G E N E R I C A P I C L I E N T I N R U B Y

• Ruby is OO in the Smalltalk tradition

• Good Ruby libraries deal in classes, objects, and methods

• Model resources with objects

• Model resource data types with classes

• Interact with links, attribute data, and embedded objects using method calls

• Everything that can be done implicitly, should

I N T R O D U C I N G H Y P E R R E S O U R C E

{ “message”: “Hello!”, “_data_type”: “Root”, “_links”: { “self”: {“href”: “/“}, “users”: {“href”: “/users{?email}, “templated”: true} } }

class MyAPI < HyperResource self.root = ‘https://myapi.example.com/v1’ end

api = MyAPI.new jdoe = api.users(email: “[email protected]”).first => #<MyAPI::User:…>

G E T / U S E R S

{ “message”: “Hello! Here are some users.”, “_data_type”: “UserSet”, “_links”: { “self”: {“href”: “/users?jdoe%40example.com“}, “root”: {“href”: “/“} }, “_embedded”: { “users”: [ { “first_name”: “John”, “last_name”: “Doe”, “_data_type”: “User”, “_links”: { “self”: {“href”: “/users/22”}, “root”: {“href”: “/“} } } ] } }

I M P L I C I T C O D E E X PA N S I O N

• Any unrecognized method call on a non-loaded resource will load the resource with GET and repeat the method call

• Any unrecognized method call on a link will load the link target and repeat the method call on the object representing the returned resource

• Any unrecognized method call on a loaded resource will be cross-referenced by name against links, embedded objects, and resource attributes

C O D E E X PA N S I O N , I L L U S T R AT E D

api.users(email: “[email protected]").first

api.get.users(email: “[email protected]").first

api.get.links.users(email: “[email protected]").first

api.get.links["users"].where(email: “[email protected]”) .first

api.get.links["users"].where(email: “[email protected]”) .get.first

api.get.links["users"].where(email: “[email protected]") .get.objects.first[1][0]

E X T E N D I N G D ATA T Y P E S

• Inspired by ActiveResource

class MyAPI::User def formal_name "The Right Honorable #{first_name} #{last_name}" endend

jdoe.formal_name # => "The Right Honorable John Doe"

C R U D E X A M P L E

jdoe.first_name = “Jane"

jdoe.patch # sends changed attributes (‘first_name’) only # => #<MyAPI::User:…>

jdoe.put # sends all attributes # => #<MyAPI::User:…>

## make a new userred = api.users.post(first_name: "Red", last_name: “Shirt") # => #<MyAPI::User:…>

## unmake a new user red.delete

AT T R I B U T E F I LT E R I N G

class MyAPI < HyperResource class User < MyAPI def outgoing_body_filter(attrs) if !attrs[‘last_name’] attrs else ## Only save first initial of last name attrs.merge(‘last_name’ => attrs[‘last_name’][0]) end end endend

!jdoe.put.formal_name # => “The Right Honorable John D”

B O N U S : A P I E C O S Y S T E M

http://localhost:12345/ returns: !{ "name": "Server One", "_links": { "self": {"href": "http://localhost:12345/"}, "server_two": {"href": "http://localhost:23456/"} } } !http://localhost:23456/ returns: !{ "name": "Server Two", "_links": { "self": {"href": "http://localhost:23456/"}, "server_one": {"href": "http://localhost:12345/"} } }

B O N U S : A P I E C O S Y S T E M

class APIEcosystem < HyperResource self.config( "localhost:12345" => {"namespace" => "ServerOneAPI"}, "localhost:23456" => {"namespace" => "ServerTwoAPI"} ) end !root_one = APIEcosystem.new(root: ‘http://localhost:12345’).get root_one.name # => ‘Server One’ root_one.url # => ‘http://localhost:12345’ root_one.namespace # => ServerOneAPI !root_two = root_one.server_two.get root_two.name # => ‘Server Two’ root_two.url # => ‘http://localhost:23456’ root_two.namespace # => ServerTwoAPI

F U T U R E W O R K

• More formats: Siren, JSON-LD, Collection+JSON

• More auth schemes: OAuth, Amazon-style

• More languages: JS/ES, Scala

L I N K S

• HyperResourcehttps://github.com/gamache/hyperresource

• The paper: “Pragmatic Hypermedia”http://petegamache.com/wsrest2014-gamache.pdf

• These slides http://petegamache.com/wsrest2014-gamache-slides.pdf

[email protected]@gamache http://petegamache.com

Q U E S T I O N S ? T H A N K S !