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.
- checksums.yaml +7 -0
- data/Copyright.txt +8 -0
- data/LICENSE.txt +697 -0
- data/README.md +101 -0
- data/Rakefile +52 -0
- data/doc/Shep/Entity/Account.html +193 -0
- data/doc/Shep/Entity/Context.html +165 -0
- data/doc/Shep/Entity/CustomEmoji.html +171 -0
- data/doc/Shep/Entity/MediaAttachment.html +175 -0
- data/doc/Shep/Entity/Notification.html +171 -0
- data/doc/Shep/Entity/Status.html +217 -0
- data/doc/Shep/Entity/StatusSource.html +167 -0
- data/doc/Shep/Entity/Status_Application.html +167 -0
- data/doc/Shep/Entity/Status_Mention.html +169 -0
- data/doc/Shep/Entity/Status_Tag.html +165 -0
- data/doc/Shep/Entity.html +1457 -0
- data/doc/Shep/Error/Caller.html +147 -0
- data/doc/Shep/Error/Http.html +329 -0
- data/doc/Shep/Error/Remote.html +143 -0
- data/doc/Shep/Error/Server.html +147 -0
- data/doc/Shep/Error/Type.html +233 -0
- data/doc/Shep/Error.html +149 -0
- data/doc/Shep/Session.html +4094 -0
- data/doc/Shep.html +128 -0
- data/doc/_index.html +300 -0
- data/doc/class_list.html +51 -0
- data/doc/css/common.css +1 -0
- data/doc/css/full_list.css +58 -0
- data/doc/css/style.css +497 -0
- data/doc/file.README.html +159 -0
- data/doc/file_list.html +56 -0
- data/doc/frames.html +17 -0
- data/doc/index.html +300 -0
- data/doc/js/app.js +314 -0
- data/doc/js/full_list.js +216 -0
- data/doc/js/jquery.js +4 -0
- data/doc/method_list.html +387 -0
- data/doc/top-level-namespace.html +110 -0
- data/lib/shep/entities.rb +164 -0
- data/lib/shep/entity_base.rb +378 -0
- data/lib/shep/exceptions.rb +78 -0
- data/lib/shep/session.rb +970 -0
- data/lib/shep/typeboxes.rb +180 -0
- data/lib/shep.rb +22 -0
- data/run_rake_test.example.sh +46 -0
- data/shep.gemspec +28 -0
- data/spec/data/smallimg.jpg +0 -0
- data/spec/data/smallish.jpg +0 -0
- data/spec/entity_common.rb +120 -0
- data/spec/entity_t1_spec.rb +168 -0
- data/spec/entity_t2_spec.rb +123 -0
- data/spec/entity_t3_spec.rb +30 -0
- data/spec/json_objects/account.1.json +25 -0
- data/spec/json_objects/account.2.json +36 -0
- data/spec/json_objects/status.1.json +85 -0
- data/spec/json_objects/status.2.json +59 -0
- data/spec/json_objects/status.3.json +95 -0
- data/spec/json_objects/status.4.json +95 -0
- data/spec/json_objects/status.5.json +74 -0
- data/spec/json_objects/status.6.json +140 -0
- data/spec/json_objects/status.7.json +84 -0
- data/spec/session_reader_1_unauth_spec.rb +366 -0
- data/spec/session_reader_2_auth_spec.rb +96 -0
- data/spec/session_writer_spec.rb +183 -0
- data/spec/spec_helper.rb +73 -0
- data/yard_helper.rb +30 -0
- 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
|