shep 0.1.0.pre.alpha0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. checksums.yaml +7 -0
  2. data/Copyright.txt +8 -0
  3. data/LICENSE.txt +697 -0
  4. data/README.md +101 -0
  5. data/Rakefile +52 -0
  6. data/doc/Shep/Entity/Account.html +193 -0
  7. data/doc/Shep/Entity/Context.html +165 -0
  8. data/doc/Shep/Entity/CustomEmoji.html +171 -0
  9. data/doc/Shep/Entity/MediaAttachment.html +175 -0
  10. data/doc/Shep/Entity/Notification.html +171 -0
  11. data/doc/Shep/Entity/Status.html +217 -0
  12. data/doc/Shep/Entity/StatusSource.html +167 -0
  13. data/doc/Shep/Entity/Status_Application.html +167 -0
  14. data/doc/Shep/Entity/Status_Mention.html +169 -0
  15. data/doc/Shep/Entity/Status_Tag.html +165 -0
  16. data/doc/Shep/Entity.html +1457 -0
  17. data/doc/Shep/Error/Caller.html +147 -0
  18. data/doc/Shep/Error/Http.html +329 -0
  19. data/doc/Shep/Error/Remote.html +143 -0
  20. data/doc/Shep/Error/Server.html +147 -0
  21. data/doc/Shep/Error/Type.html +233 -0
  22. data/doc/Shep/Error.html +149 -0
  23. data/doc/Shep/Session.html +4094 -0
  24. data/doc/Shep.html +128 -0
  25. data/doc/_index.html +300 -0
  26. data/doc/class_list.html +51 -0
  27. data/doc/css/common.css +1 -0
  28. data/doc/css/full_list.css +58 -0
  29. data/doc/css/style.css +497 -0
  30. data/doc/file.README.html +159 -0
  31. data/doc/file_list.html +56 -0
  32. data/doc/frames.html +17 -0
  33. data/doc/index.html +300 -0
  34. data/doc/js/app.js +314 -0
  35. data/doc/js/full_list.js +216 -0
  36. data/doc/js/jquery.js +4 -0
  37. data/doc/method_list.html +387 -0
  38. data/doc/top-level-namespace.html +110 -0
  39. data/lib/shep/entities.rb +164 -0
  40. data/lib/shep/entity_base.rb +378 -0
  41. data/lib/shep/exceptions.rb +78 -0
  42. data/lib/shep/session.rb +970 -0
  43. data/lib/shep/typeboxes.rb +180 -0
  44. data/lib/shep.rb +22 -0
  45. data/run_rake_test.example.sh +46 -0
  46. data/shep.gemspec +28 -0
  47. data/spec/data/smallimg.jpg +0 -0
  48. data/spec/data/smallish.jpg +0 -0
  49. data/spec/entity_common.rb +120 -0
  50. data/spec/entity_t1_spec.rb +168 -0
  51. data/spec/entity_t2_spec.rb +123 -0
  52. data/spec/entity_t3_spec.rb +30 -0
  53. data/spec/json_objects/account.1.json +25 -0
  54. data/spec/json_objects/account.2.json +36 -0
  55. data/spec/json_objects/status.1.json +85 -0
  56. data/spec/json_objects/status.2.json +59 -0
  57. data/spec/json_objects/status.3.json +95 -0
  58. data/spec/json_objects/status.4.json +95 -0
  59. data/spec/json_objects/status.5.json +74 -0
  60. data/spec/json_objects/status.6.json +140 -0
  61. data/spec/json_objects/status.7.json +84 -0
  62. data/spec/session_reader_1_unauth_spec.rb +366 -0
  63. data/spec/session_reader_2_auth_spec.rb +96 -0
  64. data/spec/session_writer_spec.rb +183 -0
  65. data/spec/spec_helper.rb +73 -0
  66. data/yard_helper.rb +30 -0
  67. metadata +154 -0
@@ -0,0 +1,378 @@
1
+
2
+ #
3
+ # Abstract base class for Entities. As much as possible, all of the
4
+ # smarts are here.
5
+ #
6
+
7
+
8
+
9
+
10
+ module Shep
11
+ using Assert
12
+
13
+
14
+ # Abstract base class for Mastodon objects.
15
+ #
16
+ # Mastodon provides its content as JSON hashes with documented names
17
+ # and values. Shep takes this one step further and provides a class
18
+ # for each object type. These are similar to Ruby's `Struct` but
19
+ # are also strongly typed.
20
+ #
21
+ # Typing is primarily useful for converting things that don't have
22
+ # explicit JSON types (e.g. Time, URI) into Ruby types. However, it
23
+ # will also catch the case where you're trying to set a field to
24
+ # something with the wrong type.
25
+ #
26
+ # Supported types are:
27
+ #
28
+ # * Number - (Integer but also allows Float)
29
+ # * Boolean
30
+ # * String
31
+ # * URI - (a Ruby URI object)
32
+ # * Time - parsed from and converted to ISO8601-format strings
33
+ # * Entity - an arbitrary Entity subclass
34
+ # * Array - strongly typed array of any of the above types
35
+ #
36
+ # Fields may also be set to nil, except for `Array` which must
37
+ # instead be set to an ampty array.
38
+ #
39
+ # Entities can be converted to and from Ruby Hashes. For this, we
40
+ # provide two flavours of Hash: the regular Ruby Hash where values
41
+ # are just the Ruby objects and the JSON hash where everything has
42
+ # been converted to the types expected by a Mastodon server.
43
+ #
44
+ # For JSON hashes, `Time` objects become ISO8601-formatted strings,
45
+ # `URI` objects become strings containing the url and `Entity`
46
+ # subobjects become their own JSON hashes. (Note that conversion to
47
+ # JSON hashes isn't really used outside of some testing and internal
48
+ # stuff so I don't guarantee that a Mastodon server or client will
49
+ # accept them.)
50
+ #
51
+ # Normally, we care about initializing Entity objects from the
52
+ # corresponding parsed JSON object and produce Ruby hashes when we
53
+ # need to use a feature `Hash` provides.
54
+ #
55
+ # Subclasses are all defined inside the Entity namespace so that it
56
+ # groups nicely in YARD docs (and because it makes the intent
57
+ # obvious).
58
+ class Entity
59
+
60
+ # Default constructor; creates an empty instance. You'll
61
+ # probably want to use {with} or {from} instead.
62
+ def initialize
63
+ init_fields()
64
+ end
65
+
66
+ #
67
+ # Instance creation
68
+ #
69
+
70
+ # Construct an instance initialized with Ruby objects.
71
+ #
72
+ # This intended for creating {Entity} subobjects in Ruby code.
73
+ # Keys of {fields} must correspond to the class's supported fields
74
+ # and be of the correct type. No fields may be omitted.
75
+ def self.with(**fields) =
76
+ new.set_from_hash!(fields,
77
+ ignore_unknown: false,
78
+ from_json: false)
79
+
80
+ # Construct an instance from the (parsed) JSON object returned by
81
+ # Mastodon.
82
+ #
83
+ # Values must be of the expected types as they appear in the
84
+ # Mastodon object (i.e. the JSON Hash described above). Missing
85
+ # key/value pairs are allowed and treated as nil; unknown keys are
86
+ # ignored.
87
+ def self.from(json_hash) =
88
+ new.set_from_hash!(json_hash, ignore_unknown: true, from_json: true)
89
+
90
+
91
+ # Set all fields from a hash.
92
+ #
93
+ # This is the back-end for {from} and {with}.
94
+ #
95
+ # @param some_hash [Hash] the Hash containing the contents
96
+ #
97
+ # @param ignore_unknown [Bool] if false, unknown keys cause an error
98
+ #
99
+ # @param from_json [Bool] if true, expect values in the format
100
+ # provided by the Mastodon API and convert
101
+ # accordingly. Otherwise, expect Ruby types.
102
+ #
103
+ def set_from_hash!(some_hash, ignore_unknown: false, from_json: false)
104
+ some_hash.each do |key, value|
105
+ key = key.intern
106
+ unless has_fld?(key)
107
+ raise Error::Caller.new("Unknown field: '#{key}'!") unless
108
+ ignore_unknown
109
+ next
110
+ end
111
+
112
+ if from_json
113
+ getbox(key).set_from_json(value)
114
+ else
115
+ self.send("#{key}=".intern, value)
116
+ end
117
+ end
118
+
119
+ return self
120
+ end
121
+
122
+
123
+ #
124
+ # Printing
125
+ #
126
+
127
+ # Produce a **short** human-friendly description.
128
+ #
129
+ # @return [String]
130
+ def to_s
131
+ notable = self.disp_fields()
132
+ .reject{|fld| getbox(fld).get_for_json.to_s.empty? }
133
+ .map{|fld| "#{fld}=#{getbox(fld).get}"}
134
+ .join(",")
135
+ notable = "0x#{self.object_id.to_s(16)}" if notable.empty?
136
+
137
+ "#{self.class}<#{notable}>"
138
+ end
139
+ alias inspect to_s
140
+
141
+ protected
142
+
143
+ def self.disp_fields = []
144
+
145
+ public
146
+
147
+ #
148
+ # Basic access
149
+ #
150
+
151
+ # Retrieve a field value by name
152
+ #
153
+ # @param key [String,Symbol]
154
+ #
155
+ # @return [Object]
156
+ def [](key) = getbox(key).get
157
+
158
+ # Set a field value by name
159
+ #
160
+ # @param key [String,Symbol]
161
+ # @param value [Object]
162
+ #
163
+ # @return [Object] the `value` parameter
164
+ def []=(key, value)
165
+ getbox(key).set(value)
166
+ return value
167
+ end
168
+
169
+ # Compare for equality.
170
+ #
171
+ # Two Entity subinstances are identical if they are of the same
172
+ # class and all of their field values are also equal according to
173
+ # `:==`
174
+ #
175
+ # @return [Boolean]
176
+ def ==(other)
177
+ return false unless self.class == other.class
178
+ keys.each{|k| return false unless self[k] == other[k] }
179
+ return true
180
+ end
181
+
182
+ # Return a hash of the contents mapping field name to value.
183
+ #
184
+ # @param json_compatible [Boolean] if true, convert to JSON-friendly form
185
+ #
186
+ # If `json_compatible` is true, the resulting hash will be
187
+ # easily convertable to Mastodon-format JSON. See above.
188
+ #
189
+ # Unset (i.e. nil) values appear as entries with nil values.
190
+ #
191
+ # @return [Hash]
192
+ def to_h(json_compatible = false)
193
+ result = {}
194
+
195
+ keys.each{|name|
196
+ hkey = json_compatible ? name.to_s : name
197
+
198
+ box = getbox(name)
199
+ val = json_compatible ? box.get_for_json : box.get
200
+
201
+ result[hkey] = val
202
+ }
203
+
204
+ result
205
+ end
206
+
207
+ # Produce a long-form human-friendly description of this Entity.
208
+ #
209
+ # This is mostly here for debugging.
210
+ #
211
+ # @return [String]
212
+ def to_long_s(indent_level = 0)
213
+ name_pad = keys.map(&:size).max + 2
214
+
215
+ result = keys.map do |key|
216
+ line = " " * (indent_level * 2)
217
+ line += sprintf("%-*s", name_pad, "#{key}:")
218
+
219
+ val = self[key]
220
+ line += val.is_a?(Entity) ? val.to_long_s(indent_level + 1) : val.to_s
221
+ end
222
+
223
+ return result.join("\n")
224
+ end
225
+
226
+ # Wrapper around `puts to_long_s()`
227
+ def print = puts(to_long_s)
228
+
229
+
230
+ #
231
+ # Subclass definition via cool metaprogramming
232
+ #
233
+
234
+
235
+ # Cool metaprogramming thing for defining {Entity} subclasses.
236
+ #
237
+ # A typical {Entity} subclass should contain only a call to this
238
+ # method. For example:
239
+ #
240
+ # class Thingy < Entity
241
+ # fields(
242
+ # :id, %i{show}, StringBox,
243
+ # :timestamp, TimeBox,
244
+ # :count, %i{show}, NumBox,
245
+ # :url, URIBox,
246
+ # )
247
+ # end
248
+ #
249
+ # {fields} takes a variable sequence of arguments that must be
250
+ # grouped as follows:
251
+ #
252
+ # 1. The field name. This **must** be a symbol.
253
+ #
254
+ # 2. An optional Array containing the symbol :show. If given,
255
+ # this field will be included in the string returned by
256
+ # `to_s`. (This is actually a mechanism for setting various
257
+ # properties, but all we need is `:show`, so that's it for
258
+ # now.)
259
+ #
260
+ # 3. The type specifier. If omitted, defaults to StringBox.
261
+ #
262
+ # The type specifier must be either:
263
+ #
264
+ # 1. One of the following classes: `TypeBox`, `StringBox`,
265
+ # `TimeBox`, `URIBox`, or `NumBox`, corresponding to the type
266
+ # this field will be.
267
+ #
268
+ # 2. A subclass of Entity, indicating that this field holds
269
+ # another Mastodon object.
270
+ #
271
+ # 3. An Array holding a single element which must be one of the
272
+ # above classes, indicating that the field holds an array of
273
+ # items of that type.
274
+ #
275
+ # @api private
276
+ def self.fields(*flds)
277
+ known_props = %i{show}.to_set
278
+
279
+ names_and_types = []
280
+ notables = []
281
+ until flds.empty?
282
+ name = flds.shift
283
+ assert{ name.class == Symbol }
284
+
285
+ properties = Set.new
286
+ if flds[0].is_a?(Array) && flds[0][0].is_a?(Symbol)
287
+ properties += flds[0]
288
+
289
+ assert("Unknown properti(es): #{(properties - known_props).to_a}") {
290
+ (properties - known_props).empty?
291
+ }
292
+
293
+ flds.shift
294
+ end
295
+
296
+ notables.push(name) if properties.include? :show
297
+
298
+ if flds[0] && flds[0].class != Symbol
299
+ typefld = flds.shift
300
+
301
+ # Array means ArrayBox with the element as type
302
+ if typefld.is_a? Array
303
+ assert{typefld.size == 1 && typefld[0].is_a?(Class)}
304
+ atype = typefld[0]
305
+
306
+ # If this is an array of entity boxes, handle that
307
+ atype = EntityBox.wrapping(atype) if atype < Entity
308
+
309
+ type = ArrayBox.wrapping(atype)
310
+
311
+ elsif typefld.is_a?(Class) && typefld < Entity
312
+ type = EntityBox.wrapping(typefld)
313
+
314
+ elsif typefld.is_a?(Class) && typefld < TypeBox
315
+ type = typefld
316
+
317
+ else
318
+ raise Error::Caller.new("Unknown field type '#{typefld}'")
319
+ end
320
+ else
321
+ type = StringBox
322
+ end
323
+
324
+ add_fld(name, type)
325
+ names_and_types.push [name, type]
326
+ end
327
+
328
+ names_and_types.freeze
329
+ notables.freeze
330
+
331
+ add_init(names_and_types)
332
+ make_has_name(names_and_types)
333
+ make_disp_fields(notables)
334
+ make_keys(names_and_types)
335
+
336
+ # This gets used to generate documentation so we make it private
337
+ # and (ab)use Object.send to call it later
338
+ define_singleton_method(:names_and_types) { names_and_types }
339
+ singleton_class.send(:private, :names_and_types)
340
+ end
341
+
342
+
343
+ private
344
+
345
+ def self.make_keys(names_and_types)
346
+ keys = names_and_types.map{|n, t| n}.freeze
347
+ define_method(:keys) { keys }
348
+ end
349
+
350
+ def self.make_has_name(names_and_types)
351
+ names_set = names_and_types.map{|n, t| n}.to_set.freeze
352
+ define_method(:has_fld?) {|name| return names_set.include?(name) }
353
+ end
354
+
355
+ def self.make_disp_fields(notables)
356
+ define_method(:disp_fields) { notables }
357
+ protected(:disp_fields)
358
+ end
359
+
360
+ def self.add_init(names_and_types)
361
+ define_method(:init_fields) {
362
+ for name, box_type in names_and_types
363
+ box = box_type.new("#{self.class}.#{name}")
364
+ instance_variable_set("@#{name}".intern, box)
365
+ end
366
+ }
367
+ end
368
+
369
+ def self.add_fld(name, type)
370
+ self.define_method(name) { getbox(name).get }
371
+ self.define_method("#{name}=".intern) { |val| getbox(name).set(val) }
372
+ end
373
+
374
+ protected
375
+
376
+ def getbox(name) = instance_variable_get("@#{name}".intern)
377
+ end
378
+ end
@@ -0,0 +1,78 @@
1
+
2
+ #
3
+ # Exception classes and error checking code used by Shep
4
+ #
5
+
6
+
7
+
8
+ module Shep
9
+
10
+ # Base class for exceptions originating from Shep. Use this to trap
11
+ # all Shep exceptions at once.
12
+ class Error < RuntimeError; end
13
+
14
+ # Error caused by using the interface incorrectly. *Not* the same
15
+ # as incorrect input data (usually)
16
+ class Error::Caller < Error; end
17
+
18
+ # Error caused by assigning a value of the wrong type to an
19
+ # `Entity` field.
20
+ class Error::Type < Error::Caller
21
+ def initialize(boxdesc, got, *want)
22
+ super("#{boxdesc} expects #{want.join(',')}; got #{got}")
23
+ end
24
+ end
25
+
26
+ # Error caused by network issues.
27
+ class Error::Remote < Error; end
28
+
29
+ # Thrown when the server doesn't like what we did. (Different
30
+ # from Error::Remote because this assumes the server is working
31
+ # correctly.)
32
+ class Error::Server < Error; end
33
+
34
+ # Thrown when the HTTP library returns an error response.
35
+ #
36
+ # Basically the same meaning as Error::Server but also includes
37
+ # the response object for your perusal.
38
+ class Error::Http < Error::Server
39
+ attr_reader :response
40
+ def initialize(response)
41
+ msg = "HTTP Error #{response.class}"
42
+
43
+ errmsg = find_error_msg_if_present(response)
44
+ msg += ": #{errmsg}" if errmsg != ""
45
+
46
+ super(msg)
47
+ @response = response
48
+ end
49
+
50
+ private
51
+
52
+ def find_error_msg_if_present(response)
53
+ return "" unless response.class.body_permitted?
54
+ obj = JSON.parse(response.body)
55
+ return obj["error"] || ""
56
+ rescue JSON::JSONError
57
+ return ""
58
+ end
59
+ end
60
+
61
+
62
+ private
63
+
64
+ # We use refinement to add 'assert' to object. This gets used a lot
65
+ # for internal error checking.
66
+ #
67
+ # @private
68
+ module Assert
69
+ refine Object do
70
+ def assert(msg = nil, &block)
71
+ return if block.call
72
+ msg = "Check failed" unless msg
73
+ msg = block.source_location.join(':') + " - " + msg
74
+ raise Error::Caller.new(msg)
75
+ end
76
+ end
77
+ end
78
+ end