~leonardr/lazr.restful/fix-ws-accept-on-redirect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
Entries
*******

Most objects published by a lazr.restful web service are entries:
self-contained data structures with an independent existence from any
other entry. Entries are distinguished from collections, which are
groupings of entries.

All entries in a web service work pretty much the same way. This
document illustrates the general features of entries, using the
example web service's dishes and recipes as examples.

    >>> from lazr.restful.testing.webservice import WebServiceCaller
    >>> webservice = WebServiceCaller(domain='cookbooks.dev')

=======
Reading
=======

It's possible to get a JSON 'representation' of an entry by sending a
GET request to the entry's URL.

Here we see that the cookbook 'Everyday Greens' is a vegetarian cookbook.

    >>> from urllib import quote
    >>> greens_url = quote("/cookbooks/Everyday Greens")
    >>> webservice.get(greens_url).jsonBody()['cuisine']
    u'Vegetarian'

Data is served encoded in UTF-8, and a good client will automatically
convert it into Unicode.

    >>> construsions_url = quote("/cookbooks/Construsions un repas")
    >>> webservice.get(construsions_url).jsonBody()['cuisine']
    u'Fran\xe7aise'

Content negotiation
===================

By varying the 'Accept' header, the client can request either a JSON
or XHTML representation of an entry, or a WADL description of the
entry's capabilities.

    >>> def negotiated_type(accept_header,
    ...                     uri='/cookbooks/Everyday%20Greens'):
    ...     return webservice.get(
    ...         uri, accept_header).getheader('Content-Type')

    >>> negotiated_type('application/json')
    'application/json'

    >>> negotiated_type('application/xhtml+xml')
    'application/xhtml+xml'

    >>> negotiated_type('application/vnd.sun.wadl+xml')
    'application/vnd.sun.wadl+xml'

    >>> negotiated_type(None)
    'application/json'

    >>> negotiated_type('text/html')
    'application/json'

    >>> negotiated_type('application/json, application/vnd.sun.wadl+xml')
    'application/json'

    >>> negotiated_type('application/json, application/xhtml+xml')
    'application/json'

    >>> negotiated_type('application/vnd.sun.wadl+xml, text/html, '
    ...                 'application/json')
    'application/vnd.sun.wadl+xml'

    >>> negotiated_type('application/json;q=0.5, application/vnd.sun.wadl+xml')
    'application/vnd.sun.wadl+xml'

    >>> negotiated_type('application/json;q=0, application/xhtml+xml;q=0.05,'
    ...                 'application/vd.sun.wadl+xml;q=0.1')
    'application/vd.sun.wadl+xml'

The client can also set the 'ws.accept' query string variable, which
will take precedence over any value set for the Accept header.

    >>> def qs_negotiated_type(query_string, header):
    ...     uri = '/cookbooks/Everyday%20Greens?ws.accept=' + query_string
    ...     return negotiated_type(header, uri)

    >>> qs_negotiated_type('application/json', None)
    'application/json'

    >>> qs_negotiated_type('application/json', 'application/xhtml+xml')
    'application/json'

    >>> negotiated_type('application/json;q=0, application/xhtml+xml;q=0.5,'
    ...                 'application/json;q=0.5, application/xhtml+xml;q=0,')
    'application/xhtml+xml'

Earlier versions of lazr.restful served a misspelling of the WADL
media type. For purposes of backwards compatibility, lazr.restful
will still serve this media type if it's requested.

    >>> negotiated_type('application/vd.sun.wadl+xml')
    'application/vd.sun.wadl+xml'

XHTML representations
=====================

Every entry has an XHTML representation. The default representation is
a simple definition list.

    >>> print webservice.get(greens_url, 'application/xhtml+xml')
    HTTP/1.1 200 Ok
    ...
    <dl ...>
    ...
    </dl>

Getting the XHTML representation works correctly even when some of the fields
have non-ascii values.

    >>> print webservice.get(construsions_url, 'application/xhtml+xml')
    HTTP/1.1 200 Ok
    ...
    <dl ...>
    ...
    <BLANKLINE>
     <dt>cuisine</dt>
     <dd>Française</dd>
    <BLANKLINE>
    ...
    </dl>

But it's possible to define a custom HTML view for a particular object
type. Here's a simple view that serves some hard-coded HTML.

    >>> class DummyView:
    ...
    ...     def __init__(*args):
    ...         pass
    ...
    ...     def __call__(*args):
    ...         return "<html>foo</html>"

Register the view as the IWebServiceClientRequest view for an
ICookbook entry...

    >>> from lazr.restful.interfaces import IWebServiceClientRequest
    >>> from lazr.restful.example.base.interfaces import ICookbook
    >>> from zope.interface.interfaces import IInterface
    >>> view_name = "lazr.restful.EntryResource"
    >>> from zope.component import getGlobalSiteManager
    >>> manager = getGlobalSiteManager()
    >>> manager.registerAdapter(
    ...      factory=DummyView,
    ...      required=[ICookbook, IWebServiceClientRequest],
    ...      provided=IInterface, name=view_name)

...and the XHTML representation of an ICookbook will be the result of
calling a DummyView object.

    >>> print webservice.get(greens_url, 'application/xhtml+xml')
    HTTP/1.1 200 Ok
    ...
    <html>foo</html>

Before we continue, here's some cleanup code to remove the custom view
we just defined.

    >>> from zope.component import getGlobalSiteManager
    >>> ignored = getGlobalSiteManager().unregisterAdapter(
    ...      factory=DummyView,
    ...      required=[ICookbook, IWebServiceClientRequest],
    ...      provided=IInterface, name=view_name)

    >>> print webservice.get(greens_url, 'application/xhtml+xml')
    HTTP/1.1 200 Ok
    ...
    <dl ...>
    ...
    </dl>

Visibility
==========

There are two recipes in "James Beard's American Cookery", but one of
them has been marked private. The private one cannot be retrieved.

    >>> print webservice.get('/recipes/3')
    HTTP/1.1 200 Ok
    ...

    >>> print webservice.get('/recipes/5')
    HTTP/1.1 401 Unauthorized
    ...

If a resource itself is visible to the client, but it contains
information that's not visible, the information will be
redacted. Here, a cookbook resource is visible to our client, but its
value for the 'confirmed' field is not visible.

    >>> cookbook = webservice.get(greens_url).jsonBody()
    >>> print cookbook['name']
    Everyday Greens
    >>> print cookbook['confirmed']
    tag:launchpad.net:2008:redacted

Named operations
================

Some entries support custom operations through GET. The custom
operation to be invoked is named in the query string's 'ws.op'
argument. You can search a cookbook's recipes by specifying
the 'find_recipes' operation.

    >>> joy_url = quote("/cookbooks/The Joy of Cooking")
    >>> recipes = webservice.get(
    ...     "%s?ws.op=find_recipes&search=e" % joy_url).jsonBody()
    >>> sorted([r['self_link'] for r in recipes['entries']])
    [u'.../recipes/2', u'.../recipes/4']

A named operation can take as an argument the URL to another
object. Here the 'dish' argument is the URL to a dish, and the named
operation finds a recipe for making that dish.

    >>> def find_recipe_in_joy(dish_url):
    ...     """Look up a dish in 'The Joy of Cooking'."""
    ...     return webservice.get("%s?ws.op=find_recipe_for&dish=%s" %
    ...         (joy_url, quote(dish_url))).jsonBody()

    >>> dish_url = webservice.get("/recipes/2").jsonBody()['dish_link']
    >>> find_recipe_in_joy(dish_url)['instructions']
    u'Draw, singe, stuff, and truss...'

The URL passed in to a named operation may be an absolute URL, or it
may be relative to the versioned service root. This is for developer
convenience only, as lazr.restful never serves relative URLs.

    >>> print dish_url
    http://cookbooks.dev/devel/dishes/Roast%20chicken
    >>> relative_url = quote("/dishes/Roast chicken")
    >>> find_recipe_in_joy(relative_url)['instructions']
    u'Draw, singe, stuff, and truss...'

A URL relative to the unversioned service root will not work.

    >>> relative_url = quote("/devel/dishes/Roast chicken")
    >>> find_recipe_in_joy(relative_url)
    Traceback (most recent call last):
    ...
    ValueError: dish: No such object "/devel/dishes/Roast%20chicken".


Some entries support custom operations through POST. You can invoke a
custom operation to modify a cookbook's name, making it seem more
interesting.

    >>> print webservice.get(joy_url).jsonBody()['cuisine']
    General

    >>> print webservice.named_post(joy_url, 'make_more_interesting', {})
    HTTP/1.1 200 Ok
    ...

    >>> new_joy_url = quote("/cookbooks/The New The Joy of Cooking")
    >>> print webservice.get(new_joy_url).jsonBody()['name']
    The New The Joy of Cooking

Custom operations may have error handling.

    >>> print webservice.named_post(new_joy_url, 'make_more_interesting', {})
    HTTP/1.1 400 Bad Request
    ...
    The 'New' trick can't be used on this cookbook because its
    name already starts with 'The New'.
    ...

    >>> import simplejson
    >>> ignore = webservice.patch(
    ...     new_joy_url, 'application/json',
    ...     simplejson.dumps({"name": "The Joy of Cooking"}))

Trying to invoke a nonexistent custom operation yields an error.

    >>> print webservice.get("%s?ws.op=no_such_operation" % joy_url)
    HTTP/1.1 400 Bad Request
    ...
    No such operation: no_such_operation

============
Modification
============

It's possible to modify an entry by sending to the server a document
asserting what the entry should look like. The document may only
describe part of the entry's new state, in which case the client
should use the PATCH HTTP method. Or it may completely describe the
entry's state, in which case the client should use PUT.

    >>> def modify_entry(url, representation, method, headers=None):
    ...     "A helper function to PUT or PATCH an entry."
    ...     new_headers = {'Content-type': 'application/json'}
    ...     if headers is not None:
    ...         new_headers.update(headers)
    ...     return webservice(
    ...         url, method, simplejson.dumps(representation), headers)

    >>> def modify_cookbook(cookbook, representation, method, headers=None):
    ...     "A helper function to PUT or PATCH a cookbook."
    ...     return modify_entry(
    ...         '/cookbooks/' + quote(cookbook), representation,
    ...         method, headers)

Here we use the web service to change the cuisine of the "Everyday
Greens" cookbook. The data returned is the new JSON representation of
the object.

    >>> print webservice.get(greens_url).jsonBody()['revision_number']
    0

    >>> result = modify_cookbook('Everyday Greens', {'cuisine' : 'American'},
    ...                          'PATCH')
    >>> print result
    HTTP/1.1 209 Content Returned
    ...
    Content-Type: application/json
    ...
    {...}

    >>> greens = result.jsonBody()
    >>> print greens['cuisine']
    American

Whenever a client modifies a cookbook, the revision_number is
incremented behind the scenes.

    >>> print greens['revision_number']
    1

A modification may cause one of en entry's links to point to another
object. Here, we change the 'dish_link' field of a roast chicken
recipe, turning it into a recipe for baked beans.

    >>> old_dish = webservice.get("/recipes/1").jsonBody()['dish_link']
    >>> print old_dish
    http://.../dishes/Roast%20chicken

    >>> new_dish = webservice.get("/recipes/4").jsonBody()['dish_link']
    >>> print new_dish
    http://.../dishes/Baked%20beans

    >>> new_entry = modify_entry(
    ...     "/recipes/2", {'dish_link' : new_dish}, 'PATCH').jsonBody()
    >>> print new_entry['dish_link']
    http://.../dishes/Baked%20beans

When changing one of an entry's links, you can use an absolute URL (as
seen above) or a URL relative to the versioned service root. Let's use
a relative URL to change the baked beans recipe back to a roast
chicken recipe.

    >>> relative_old_dish = quote('/dishes/Roast chicken')
    >>> new_entry = modify_entry(
    ...     "/recipes/2", {'dish_link' : relative_old_dish},
    ...     'PATCH').jsonBody()
    >>> print new_entry['dish_link']
    http://.../dishes/Roast%20chicken

A modification might cause an entry's address to change. Here we use
the web service to change the cookbook's name to 'Everyday Greens 2'.

    >>> print modify_cookbook('Everyday Greens',
    ...                       {'name' : 'Everyday Greens 2'}, 'PATCH')
    HTTP/1.1 301 Moved Permanently
    ...
    Location: http://.../Everyday%20Greens%202
    ...

At this point we can no longer manipulate this cookbook by sending
HTTP requests to http://cookbooks.dev/1.0/cookbooks/Everyday%20Greens,
because that cookbook now 'lives' at
http://cookbooks.dev/1.0/cookbooks/Everyday%20Greens%202. To change
the cookbook name back, we need to send a PATCH request to the new
address.

    >>> print modify_cookbook('Everyday Greens 2',
    ...                       {'name' : 'Everyday Greens'}, 'PATCH')
    HTTP/1.1 301 Moved Permanently
    ...
    Location: http://.../cookbooks/Everyday%20Greens
    ...

The PATCH HTTP method is useful for simple changes, but not all HTTP
clients support PATCH. It's possible to fake a PATCH request with
POST, by setting the X-HTTP-Method-Override header to "PATCH". Because
Firefox 3 mangles the Content-Type header for POST requests, you may
also set the X-Content-Type-Override header, which will override the
value of Content-Type.

    >>> print modify_cookbook('Everyday Greens',
    ...     {'cuisine' : 'General'}, 'POST',
    ...     {'X-HTTP-Method-Override' : 'PATCH',
    ...      'Content-Type': 'not-a-valid-content/type',
    ...      'X-Content-Type-Override': 'application/json'})
    HTTP/1.1 209 Content Returned
    ...

If you try to use X-HTTP-Method-Override when the underlying HTTP
method is not POST, you'll get an error.

    >>> print modify_cookbook('Everyday Greens',
    ...     {}, 'GET', {'X-HTTP-Method-Override' : 'PATCH'})
    HTTP/1.1 400 Bad Request
    ...
    X-HTTP-Method-Override can only be used with a POST request.

Even if a client supports PATCH, sometimes it's easier to GET a
document, modify it, and send it back. If you have the full document
at hand, you can use the PUT method.

We happen to have a full document from when we sent a GET request to
the 'Everday Greens' cookbook. Modifying that document and PUTting it
back is less work than constructing a new document and sending it with
PATCH. As with PATCH, a successful PUT serve the new representation of
the object that was modified.

    >>> greens = webservice.get(greens_url).jsonBody()
    >>> print greens['cuisine']
    General

    >>> greens['cuisine'] = 'Vegetarian'
    >>> print modify_cookbook('Everyday Greens', greens, 'PUT')
    HTTP/1.1 209 Content Returned
    ...
    {...}

    >>> greens = webservice.get(greens_url).jsonBody()
    >>> print greens['cuisine']
    Vegetarian

Because our patch format is the same as our representation format (a
JSON hash), any document that works with a PUT request will also work
with a PATCH request.

    >>> print modify_cookbook('Everyday Greens', greens, 'PATCH')
    HTTP/1.1 209 Content Returned
    ...

Content negotiation during modification
=======================================

When making a PATCH, you don't have to get a JSON representation
back. You can also get an HTML representation.

    >>> print modify_cookbook('Everyday Greens', {}, 'PATCH',
    ...                       headers={'Accept': 'application/xhtml+xml'})
    HTTP/1.1 209 Content Returned
    ...
    Content-Type: application/xhtml+xml
    ...
    <?xml version="1.0"?>
    ...

You can even get a WADL representation, though that's pretty useless.

    >>> headers = {'Accept':'application/vd.sun.wadl+xml'}
    >>> print modify_cookbook('Everyday Greens', {}, 'PATCH',
    ...                       headers=headers)
    HTTP/1.1 209 Content Returned
    ...
    Content-Type: application/vd.sun.wadl+xml
    ...

Server-side modification
========================

Sometimes the server will transparently modify a value sent by the
client, to clean it up or put it into a canonical form. For this
purpose, the response to a PUT or PATCH request includes a brand new
JSON representation of the object, so that the client can know whether
and which changes were made.

Here's an example. If a cookbook's description contains leading or trailing
whitespace, the whitespace will be stripped.

    >>> greens = webservice.get(greens_url).jsonBody()
    >>> greens['description']
    u''
    >>> first_etag = greens['http_etag']

Send in a name with leading or trailing whitespace and it'll be
transparently trimmed. The document returned from the POST request
will be the new representation, modified by both client and server.

    >>> greens = webservice(
    ...     greens_url, "PATCH",
    ...     simplejson.dumps({'description' : '  A description '}),
    ...     {'Content-type': 'application/json'}).jsonBody()
    >>> greens['description']
    u'A description'
    >>> greens['http_etag'] == first_etag
    False

The canonicalization works for PUT requests as well.

    >>> greens['description'] = "    Another description  "
    >>> greens = webservice(greens_url, "PUT", simplejson.dumps(greens),
    ...                     {'Content-type': 'application/json'}).jsonBody()
    >>> greens['description']
    u'Another description'

Conditional GET, PUT and PATCH
==============================

When you GET an entry you're given an ETag; an opaque string that
changes whenever the entry changes.

    >>> response = webservice.get(greens_url)
    >>> greens_etag = response.getheader('ETag')
    >>> greens = response.jsonBody()

The ETag is present in the HTTP response headers when you GET an
entry, but it's also present in the representation of the entry
itself.

    >>> greens['http_etag'] == greens_etag
    True

This is so you can get the ETags for all the entries in a collection
at once, without making a separate HTTP request for each.

    >>> cookbooks = webservice.get('/cookbooks').jsonBody()
    >>> etags = [book['http_etag'] for book in cookbooks['entries']]

The ETag provided with an entry of a collection is the same as the
ETag you'd get if you got that entry individually.

    >>> first_book = cookbooks['entries'][0]
    >>> first_book_2 = webservice.get(first_book['self_link']).jsonBody()
    >>> first_book['http_etag'] == first_book_2['http_etag']
    True

When you make a GET request, you can provide the ETag as the
If-None-Match header. This lets you save time when the resource hasn't
changed.

    >>> print webservice.get(greens_url,
    ...                      headers={'If-None-Match': greens_etag})
    HTTP/1.1 304 Not Modified
    ...

Conditional GET works the same way whether the request goes through
the web service's virtual host or through the website-level interface
designed for Ajax.

    >>> from lazr.restful.testing.webservice import WebServiceAjaxCaller
    >>> ajax = WebServiceAjaxCaller(domain='cookbooks.dev')
    >>> etag = 'dummy-etag'
    >>> response = ajax.get(greens_url, headers={'If-None-Match' : etag})
    >>> etag = response.getheader("Etag")
    >>> print ajax.get(greens_url, headers={'If-None-Match' : etag})
    HTTP/1.1 304 Not Modified
    ...

When you make a PUT or PATCH request, you can provide the ETag as the
If-Match header. This lets you detect changes that other people made
to the entry, so your changes don't overwrite theirs.

If the ETag you provide in If-Match matches the entry's current ETag,
your request goes through.

    >>> print modify_cookbook('Everyday Greens', greens, 'PATCH',
    ...                     {'If-Match' : greens_etag})
    HTTP/1.1 209 Content Returned
    ...

If the ETags don't match, it's because somebody modified the entry
after you got your copy of it. Your request will fail with status code
412.

    >>> print modify_cookbook('Everyday Greens', greens, 'PATCH',
    ...                       {'If-Match' : '"an-old-etag"'})
    HTTP/1.1 412 Precondition Failed
    ...

If you specify a number of ETags, and any of them match, your request
will go through.

    >>> greens = webservice.get(greens_url).jsonBody()
    >>> match = '"an-old-etag", %s' % greens['http_etag']
    >>> print modify_cookbook('Everyday Greens', greens, 'PATCH',
    ...                       {'If-Match' : match})
    HTTP/1.1 209 Content Returned
    ...

Both PUT and PATCH requests work this way.

    >>> print modify_cookbook('Everyday Greens', greens, 'PUT',
    ...                       {'If-Match' : 'an-old-etag'})
    HTTP/1.1 412 Precondition Failed
    ...

Conditional writes are a little more complicated
************************************************

OK, but consider the 'copyright_date' field of a cookbook. This is
published as a read-only field; the client can't change it. But it's
not read-only on the server side. What if this value changes on the
server-side? What happens to the ETag then? Does it change, causing a
conditional PATCH to fail, even if the PATCH doesn't touch that
read-only field?

    >>> greens = webservice.get(greens_url).jsonBody()
    >>> print greens['copyright_date']
    2003-01-01
    >>> etag_before_server_modification = greens['http_etag']

    >>> from lazr.restful.example.base.root import C4
    >>> greens_object = C4
    >>> old_date = greens_object.copyright_date
    >>> old_date
    datetime.date(2003, 1, 1)

Let's change the server-side value and find out.

    >>> import datetime
    >>> greens_object.copyright_date = datetime.date(2005, 12, 12)

    >>> new_greens = webservice.get(greens_url).jsonBody()
    >>> print new_greens['copyright_date']
    2005-12-12
    >>> etag_after_server_modification = new_greens['http_etag']

The ETag has indeed changed.

    >>> etag_before_server_modification == etag_after_server_modification
    False

So if we try to modify the cookbook using the old ETag, it should
fail, right?

    >>> body = {'description' : 'New description.'}
    >>> print modify_cookbook('Everyday Greens', body, 'PATCH',
    ...                       {'If-Match' : etag_before_server_modification})
    HTTP/1.1 209 Content Returned
    ...

Actually, it succeeds! How does that work? Well, the ETag consists of
two parts, separated by a dash.

    >>> read_before, write_before = etag_before_server_modification.split('-')
    >>> read_after, write_after = etag_after_server_modification.split('-')

The first part of the ETag only changes when a field the client can't
modify changes. This is the part of the ETag that changed when
copyright_date changed.

    >>> read_before == read_after
    False

The second part only changes when a field changes that the client can
modify. This part of the ETag hasn't changed.

    >>> write_before == write_after
    True

When you make a conditional write, the second part of the ETag you
provide is checked against the second part of the ETag generated on
the server. The first part of the ETag is ignored.

The point of checking the ETag is to avoid conflicts where two clients
modify the same resource. But no client can modify any of those
read-only fields, so changes to them don't matter for purposes of
avoiding conflicts.

If you hack the ETag to something that's not "two parts, separated by
a dash", lazr.restful will still handle it. (Of course, since that
ETag will never match anything, you'll always get a 412 error.)

    >>> body = {'description' : 'New description.'}
    >>> print modify_cookbook('Everyday Greens', body, 'PATCH',
    ...                       {'If-Match' : "Weird etag"})
    HTTP/1.1 412 Precondition Failed
    ...

Conditional PUT fails where a PATCH would succeed, but not because
lazr.restful rejects an old ETag. To verify this, let's change the
cookbook's copyright_date again, behind the scenes.

    >>> greens = webservice.get(greens_url).jsonBody()
    >>> old_etag = greens['http_etag']
    >>> greens_object.copyright_date = datetime.date(2005, 11, 11)

A totally bogus ETag fails with a 412 error.

    >>> greens['description'] = 'Another new description'
    >>> print modify_cookbook('Everyday Greens', greens, 'PUT',
    ...                       {'If-Match' : "Not the old ETag"})
    HTTP/1.1 412 Precondition Failed
    ...

When we use the original ETag, we don't cause a 412 error, but the PUT
request fails anyway.

    >>> print modify_cookbook('Everyday Greens', greens, 'PUT',
    ...                       {'If-Match' : old_etag})
    HTTP/1.1 400 Bad Request
    ...
    http_etag: You tried to modify a read-only attribute.
    copyright_date: You tried to modify a read-only attribute.

Rather, it's because a PUT request includes a representation of the
entire resource, and lazr.restful thinks the client is trying to
modify the fields that changed behind the scenes--in this case,
copyright_date and http_etag.

Conditional reads are *not* more complicated
********************************************

The point of the two-part ETag is to avoid spurious 412 errors when
doing conditional writes. When making a conditional _read_ request,
the condition will fail if _any_ part of the ETag is different.

    >>> new_etag = webservice.get(greens_url).jsonBody()['http_etag']

    >>> print webservice.get(
    ...     greens_url,
    ...     headers={'If-None-Match': new_etag})
    HTTP/1.1 304 Not Modified
    ...

    >>> print webservice.get(
    ...     greens_url,
    ...     headers={'If-None-Match': "changed" + new_etag})
    HTTP/1.1 200 Ok
    ...

lazr.restful checks the entire ETag on conditional GET because the
purpose of a conditional read is to avoid getting data that hasn't
changed. A server-side change to a read-only field like copyright_date
doesn't affect future client writes, but it _is_ a change to the
representation.

A bit of cleanup: restore the old value for the cookbook's
copyright_date.

    >>> greens_object.copyright_date = old_date

Changing object relationships
=============================

In addition to changing an object's data fields, you can change its
relationships to other objects. Here we change which dish a recipe is
for.

    >>> recipe_url = '/recipes/3'
    >>> recipe = webservice.get(recipe_url).jsonBody()
    >>> print recipe['dish_link']
    http://.../dishes/Roast%20chicken

    >>> def modify_dish(url, recipe, new_dish_url):
    ...     recipe['dish_link'] = new_dish_url
    ...     return webservice.put(
    ...         url, 'application/json', simplejson.dumps(recipe))

    >>> new_dish = webservice.get(quote('/dishes/Baked beans')).jsonBody()
    >>> new_dish_url = new_dish['self_link']
    >>> recipe['dish_link'] = new_dish_url
    >>> print modify_dish(recipe_url, recipe, new_dish_url)
    HTTP/1.1 209 Content Returned
    ...

    >>> recipe = webservice.get(recipe_url).jsonBody()
    >>> print recipe['dish_link']
    http://.../dishes/Baked%20beans

Identification of the dish is done by specifying a URL; a random
string won't work.

    >>> print modify_dish(recipe_url, recipe, 'A random string')
    HTTP/1.1 400 Bad Request
    ...
    dish_link: "A random string" is not a valid URI.

But not just any URL will do. It has to identify an object in the web
service.

    >>> print modify_dish(recipe_url, recipe, 'http://www.canonical.com')
    HTTP/1.1 400 Bad Request
    ...
    dish_link: No such object "http://www.canonical.com".

    >>> print modify_dish(
    ...     recipe_url, recipe,
    ...     'http://www.canonical.com/dishes/Baked%20beans')
    HTTP/1.1 400 Bad Request
    ...
    dish_link: No such object "http://www.canonical.com/dishes/Baked%20beans".

This URL would be valid, but it uses the wrong protocol (HTTPS instead
of HTTP).

    >>> https_link = recipe['dish_link'].replace('http:', 'https:')
    >>> print modify_dish(recipe_url, recipe, https_link)
    HTTP/1.1 400 Bad Request
    ...
    dish_link: No such object "https://.../Baked%20beans".

Even a URL that identifies an object in the web service won't work, if
the object isn't the right kind of object. A recipe must be for a
dish, not a cookbook:

    >>> print modify_dish(recipe_url, recipe, recipe['cookbook_link'])
    HTTP/1.1 400 Bad Request
    ...
    dish_link: Your value points to the wrong kind of object

Date formats
============

lazr.restful web services serve and parse dates in ISO 8601
format. Only UTC dates are allowed.

The tests that follow make a number of PATCH requests that include
values for a cookbook's 'copyright_date' attribute.

    >>> greens = webservice.get(greens_url).jsonBody()
    >>> greens['copyright_date']
    u'2003-01-01'

    >>> def patch_greens_copyright_date(date):
    ...     "A helper method to try and change a date field."
    ...     return modify_cookbook(
    ...         'Everyday Greens', {'copyright_date' : date}, 'PATCH')

These requests aren't actually trying to modify 'copyright_date', which
is read-only. They're asserting that 'copyright_date' is a certain
value. If the assertion succeeds (because 'copyright_date' does in fact
have that value), the response code is 200. If the assertion could not
be understood (because the date is in the wrong format), the response
code is 400, and the body is an error message about the date
format. If the assertion _fails_ (because 'copyright_date' happens to be
read-only), the response code is also 400, but the error message talks
about an attempt to modify a read-only attribute.

The two 400 error codes below are caused by a failure to understand
the assertion. The string used in the assertion might not be a date.

    >>> print patch_greens_copyright_date('dummy')
    HTTP/1.1 400 Bad Request
    ...
    copyright_date: Value doesn't look like a date.

Or it might be a date that's not in UTC.

    >>> print patch_greens_copyright_date(u'2005-06-06T00:00:00.000000+05:00')
    HTTP/1.1 400 Bad Request
    ...
    copyright_date: Time not in UTC.

There are five ways to specify UTC:

    >>> print patch_greens_copyright_date(u'2003-01-01T00:00:00.000000Z')
    HTTP/1.1 209 Content Returned
    ...

    >>> print patch_greens_copyright_date(u'2003-01-01T00:00:00.000000+00:00')
    HTTP/1.1 209 Content Returned
    ...

    >>> print patch_greens_copyright_date(u'2003-01-01T00:00:00.000000+0000')
    HTTP/1.1 209 Content Returned
    ...

    >>> print patch_greens_copyright_date(u'2003-01-01T00:00:00.000000-00:00')
    HTTP/1.1 209 Content Returned
    ...

    >>> print patch_greens_copyright_date(u'2003-01-01T00:00:00.000000-0000')
    HTTP/1.1 209 Content Returned
    ...

A value with a missing timezone is treated as UTC.

    >>> print patch_greens_copyright_date(u'2003-01-01T00:00:00.000000')
    HTTP/1.1 209 Content Returned
    ...

Less precise time measurements may also be acceptable.

    >>> print patch_greens_copyright_date(u'2003-01-01T00:00:00Z')
    HTTP/1.1 209 Content Returned
    ...

    >>> print patch_greens_copyright_date(u'2003-01-01')
    HTTP/1.1 209 Content Returned
    ...

What you can't do
=================

A document that would be acceptable as the payload of a PATCH request
might not be acceptable as the payload of a PUT request.

    >>> print modify_cookbook('Everyday Greens', {'name' : 'Greens'}, 'PUT')
    HTTP/1.1 400 Bad Request
    ...
    You didn't specify a value for the attribute 'cuisine'.

A document that's not a valid JSON document is also unacceptable.

    >>> print webservice.patch(greens_url, "application/json", "{")
    HTTP/1.1 400 Bad Request
    ...
    Entity-body was not a well-formed JSON document.

A document that's valid JSON but is not a JSON hash is unacceptable.

    >>> print modify_cookbook('Everyday Greens', 'name=Greens', 'PATCH')
    HTTP/1.1 400 Bad Request
    ...
    Expected a JSON hash.

An entry's read-only attributes can't be modified.

    >>> print modify_cookbook(
    ...     'Everyday Greens',
    ...     {'copyright_date' : u'2001-01-01T01:01:01+00:00Z'}, 'PATCH')
    HTTP/1.1 400 Bad Request
    ...
    copyright_date: You tried to modify a read-only attribute.

You can send a document that includes a value for a read-only
attribute, but it has to be the same as the current value.

    >>> print modify_cookbook(
    ...     'Everyday Greens',
    ...     {'copyright_date' : greens['copyright_date']}, 'PATCH')
    HTTP/1.1 209 Content Returned
    ...

You can't change the link to an entry's associated collection.

    >>> print modify_cookbook('Everyday Greens',
    ...                       {'recipes_collection_link' : 'dummy'},
    ...                       'PATCH')
    HTTP/1.1 400 Bad Request
    ...
    recipes_collection_link: You tried to modify a collection...

Again, you can send a document that includes a value for an associated
collection link; you just can't _change_ the value.

    >>> print modify_cookbook(
    ...     'Everyday Greens',
    ...     {'recipes_collection_link' : greens['recipes_collection_link']},
    ...     'PATCH')
    HTTP/1.1 209 Content Returned
    ...

You can't directly change an entry's URL address.

    >>> print modify_cookbook('Everyday Greens',
    ...                       {'self_link' : 'dummy'}, 'PATCH')
    HTTP/1.1 400 Bad Request
    ...
    self_link: You tried to modify a read-only attribute.

You can't directly change an entry's ETag.

    >>> print modify_cookbook('Everyday Greens',
    ...                       {'http_etag' : 'dummy'}, 'PATCH')
    HTTP/1.1 400 Bad Request
    ...
    http_etag: You tried to modify a read-only attribute.

You can't change an entry's resource type.

    >>> print modify_cookbook('Everyday Greens',
    ...                       {'resource_type_link' : 'dummy'}, 'PATCH')
    HTTP/1.1 400 Bad Request
    ...
    resource_type_link: You tried to modify a read-only attribute.

You can't refer to a link to an associated object or collection as
though it were the actual object. A cookbook has a
'recipes_collection_link', but it doesn't have 'recipes' directly.

    >>> print modify_cookbook(
    ...     'Everyday Greens', {'recipes' : 'dummy'}, 'PATCH')
    HTTP/1.1 400 Bad Request
    ...
    recipes: You tried to modify a nonexistent attribute.

A recipe has a 'dish_link', but it doesn't have a 'dish' directly.

    >>> url = quote('/cookbooks/The Joy of Cooking/Roast chicken')
    >>> print webservice.patch(url, 'application/json',
    ...     simplejson.dumps({'dish' : 'dummy'}))
    HTTP/1.1 400 Bad Request
    ...
    dish: You tried to modify a nonexistent attribute.

You can't set values that violate data integrity rules. For instance,
you can't set a required value to None.

    >>> print modify_cookbook('Everyday Greens',
    ...                       {'name' : None}, 'PATCH')
    HTTP/1.1 400 Bad Request
    ...
    name: Missing required value.

And of course you can't modify attributes that don't exist.

    >>> print modify_cookbook(
    ...     'Everyday Greens', {'nonesuch' : 'dummy'}, 'PATCH')
    HTTP/1.1 400 Bad Request
    ...
    nonesuch: You tried to modify a nonexistent attribute.

Deletion
========

Some entries may be deleted with the HTTP DELETE method. In the
example web service, recipes can be deleted.

    >>> recipe_url = "/recipes/6"
    >>> print webservice.get(recipe_url)
    HTTP/1.1 200 Ok
    ...

    >>> print webservice.delete(recipe_url)
    HTTP/1.1 200 Ok
    ...

    >>> print webservice.get(recipe_url)
    HTTP/1.1 404 Not Found
    ...