| 1 | package suite |
| 2 | |
| 3 | import "encoding/json" |
| 4 | |
| 5 | // Authorization is what S2 answers for one item, for one named asker. |
| 6 | // |
| 7 | // Doctrine §4.3: four values, none collapsible into another. Only |
| 8 | // NOT_VISIBLE may be silent; everything else that is not VISIBLE withholds |
| 9 | // loudly and names the app. |
| 10 | type Authorization string |
| 11 | |
| 12 | const ( |
| 13 | // VISIBLE: this person may read this revision. |
| 14 | VISIBLE Authorization = "VISIBLE" |
| 15 | // NOT_VISIBLE: this person may not read it. The only value a consumer |
| 16 | // may act on silently. |
| 17 | NOT_VISIBLE Authorization = "NOT_VISIBLE" |
| 18 | // STALE: the item is readable, but the app of record has moved past the |
| 19 | // revision asked about. A synchronisation condition, never a denial. |
| 20 | STALE Authorization = "STALE" |
| 21 | // UNKNOWN: the app of record did not answer usefully, or cannot vouch |
| 22 | // for the revision asked about. Withheld loudly. |
| 23 | UNKNOWN Authorization = "UNKNOWN" |
| 24 | ) |
| 25 | |
| 26 | // ItemRevision is one pair in an S2 question: which item, at which revision. |
| 27 | type ItemRevision struct { |
| 28 | ItemID string `json:"item_id"` |
| 29 | Revision int64 `json:"revision"` |
| 30 | } |
| 31 | |
| 32 | type s2Result struct { |
| 33 | ItemID *string `json:"item_id"` |
| 34 | Revision *int64 `json:"revision"` |
| 35 | Result *Authorization `json:"result"` |
| 36 | } |
| 37 | |
| 38 | type s2Body struct { |
| 39 | Results []s2Result `json:"results"` |
| 40 | } |
| 41 | |
| 42 | // DecodeS2 turns an S2 answer into exactly one Authorization per pair asked, |
| 43 | // in the order they were asked. |
| 44 | func DecodeS2(asked []ItemRevision, status int, body []byte) []Authorization { |
| 45 | // Whole-call failures. A body that arrives with a non-2xx status is not |
| 46 | // an answer however well it parses, and a body that does not parse is |
| 47 | // not an answer however it arrived. |
| 48 | if status < 200 || status > 299 { |
| 49 | return allUnknown(len(asked)) |
| 50 | } |
| 51 | // Before decoding, not after: a duplicate key is resolved by encoding/json |
| 52 | // and gone by the time the duplicate-row rule below could see it. |
| 53 | if err := rejectAmbiguousJSON(body); err != nil { |
| 54 | return allUnknown(len(asked)) |
| 55 | } |
| 56 | var parsed s2Body |
| 57 | if err := json.Unmarshal(body, &parsed); err != nil { |
| 58 | return allUnknown(len(asked)) |
| 59 | } |
| 60 | |
| 61 | byPair := make(map[ItemRevision]Authorization, len(parsed.Results)) |
| 62 | for _, r := range parsed.Results { |
| 63 | if r.ItemID == nil || r.Revision == nil || r.Result == nil { |
| 64 | return allUnknown(len(asked)) |
| 65 | } |
| 66 | pair := ItemRevision{ItemID: *r.ItemID, Revision: *r.Revision} |
| 67 | if _, already := byPair[pair]; already { |
| 68 | // Answered twice. Two answers are not an answer, and choosing |
| 69 | // between them would be this client making a decision the app of |
| 70 | // record did not make. |
| 71 | byPair[pair] = UNKNOWN |
| 72 | continue |
| 73 | } |
| 74 | byPair[pair] = known(*r.Result) |
| 75 | } |
| 76 | |
| 77 | out := make([]Authorization, len(asked)) |
| 78 | for i, pair := range asked { |
| 79 | answer, answered := byPair[pair] |
| 80 | if !answered { |
| 81 | answer = UNKNOWN |
| 82 | } |
| 83 | out[i] = answer |
| 84 | } |
| 85 | return out |
| 86 | } |
| 87 | |
| 88 | // known maps anything outside the closed four-value vocabulary to UNKNOWN. |
| 89 | // A fifth word is a sibling this client does not understand; passing it |
| 90 | // through would hand a caller a value it cannot switch on. |
| 91 | func known(a Authorization) Authorization { |
| 92 | switch a { |
| 93 | case VISIBLE, NOT_VISIBLE, STALE, UNKNOWN: |
| 94 | return a |
| 95 | } |
| 96 | return UNKNOWN |
| 97 | } |
| 98 | |
| 99 | func allUnknown(n int) []Authorization { |
| 100 | out := make([]Authorization, n) |
| 101 | for i := range out { |
| 102 | out[i] = UNKNOWN |
| 103 | } |
| 104 | return out |
| 105 | } |
| 106 | |