tentd 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/.rspec +1 -0
- data/.travis.yml +8 -0
- data/Gemfile +9 -0
- data/Guardfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +49 -0
- data/Rakefile +8 -0
- data/bin/tent-server +3 -0
- data/lib/tentd.rb +31 -0
- data/lib/tentd/api.rb +58 -0
- data/lib/tentd/api/apps.rb +196 -0
- data/lib/tentd/api/authentication_finalize.rb +12 -0
- data/lib/tentd/api/authentication_lookup.rb +27 -0
- data/lib/tentd/api/authentication_verification.rb +50 -0
- data/lib/tentd/api/authorizable.rb +21 -0
- data/lib/tentd/api/authorization.rb +14 -0
- data/lib/tentd/api/core_profile_data.rb +45 -0
- data/lib/tentd/api/followers.rb +218 -0
- data/lib/tentd/api/followings.rb +241 -0
- data/lib/tentd/api/groups.rb +161 -0
- data/lib/tentd/api/middleware.rb +32 -0
- data/lib/tentd/api/posts.rb +373 -0
- data/lib/tentd/api/profile.rb +78 -0
- data/lib/tentd/api/router.rb +123 -0
- data/lib/tentd/api/router/caching_headers.rb +49 -0
- data/lib/tentd/api/router/extract_params.rb +88 -0
- data/lib/tentd/api/router/serialize_response.rb +38 -0
- data/lib/tentd/api/user_lookup.rb +10 -0
- data/lib/tentd/core_ext/hash/slice.rb +29 -0
- data/lib/tentd/datamapper/array_property.rb +23 -0
- data/lib/tentd/datamapper/query.rb +19 -0
- data/lib/tentd/json_patch.rb +181 -0
- data/lib/tentd/model.rb +30 -0
- data/lib/tentd/model/app.rb +68 -0
- data/lib/tentd/model/app_authorization.rb +113 -0
- data/lib/tentd/model/follower.rb +105 -0
- data/lib/tentd/model/following.rb +100 -0
- data/lib/tentd/model/group.rb +24 -0
- data/lib/tentd/model/mention.rb +19 -0
- data/lib/tentd/model/notification_subscription.rb +56 -0
- data/lib/tentd/model/permissible.rb +227 -0
- data/lib/tentd/model/permission.rb +28 -0
- data/lib/tentd/model/post.rb +178 -0
- data/lib/tentd/model/post_attachment.rb +27 -0
- data/lib/tentd/model/post_version.rb +64 -0
- data/lib/tentd/model/profile_info.rb +80 -0
- data/lib/tentd/model/random_public_id.rb +46 -0
- data/lib/tentd/model/serializable.rb +58 -0
- data/lib/tentd/model/type_properties.rb +36 -0
- data/lib/tentd/model/user.rb +39 -0
- data/lib/tentd/model/user_scoped.rb +14 -0
- data/lib/tentd/notifications.rb +13 -0
- data/lib/tentd/notifications/girl_friday.rb +30 -0
- data/lib/tentd/notifications/sidekiq.rb +50 -0
- data/lib/tentd/tent_type.rb +20 -0
- data/lib/tentd/tent_version.rb +41 -0
- data/lib/tentd/version.rb +3 -0
- data/spec/fabricators/app_authorizations_fabricator.rb +5 -0
- data/spec/fabricators/apps_fabricator.rb +11 -0
- data/spec/fabricators/followers_fabricator.rb +14 -0
- data/spec/fabricators/followings_fabricator.rb +17 -0
- data/spec/fabricators/groups_fabricator.rb +3 -0
- data/spec/fabricators/mentions_fabricator.rb +3 -0
- data/spec/fabricators/notification_subscriptions_fabricator.rb +4 -0
- data/spec/fabricators/permissions_fabricator.rb +1 -0
- data/spec/fabricators/post_attachments_fabricator.rb +8 -0
- data/spec/fabricators/post_versions_fabricator.rb +12 -0
- data/spec/fabricators/posts_fabricator.rb +12 -0
- data/spec/fabricators/profile_infos_fabricator.rb +30 -0
- data/spec/integration/api/apps_spec.rb +466 -0
- data/spec/integration/api/followers_spec.rb +535 -0
- data/spec/integration/api/followings_spec.rb +688 -0
- data/spec/integration/api/groups_spec.rb +207 -0
- data/spec/integration/api/posts_spec.rb +874 -0
- data/spec/integration/api/profile_spec.rb +285 -0
- data/spec/integration/api/router_spec.rb +102 -0
- data/spec/integration/model/app_authorization_spec.rb +59 -0
- data/spec/integration/model/app_spec.rb +63 -0
- data/spec/integration/model/follower_spec.rb +344 -0
- data/spec/integration/model/following_spec.rb +97 -0
- data/spec/integration/model/group_spec.rb +39 -0
- data/spec/integration/model/notification_subscription_spec.rb +145 -0
- data/spec/integration/model/post_spec.rb +658 -0
- data/spec/spec_helper.rb +37 -0
- data/spec/support/expect_server.rb +3 -0
- data/spec/support/json_request.rb +54 -0
- data/spec/support/with_constant.rb +23 -0
- data/spec/support/with_warnings.rb +6 -0
- data/spec/unit/api/authentication_finalize_spec.rb +45 -0
- data/spec/unit/api/authentication_lookup_spec.rb +65 -0
- data/spec/unit/api/authentication_verification_spec.rb +50 -0
- data/spec/unit/api/authorizable_spec.rb +50 -0
- data/spec/unit/api/authorization_spec.rb +44 -0
- data/spec/unit/api/caching_headers_spec.rb +121 -0
- data/spec/unit/core_profile_data_spec.rb +64 -0
- data/spec/unit/json_patch_spec.rb +407 -0
- data/spec/unit/tent_type_spec.rb +28 -0
- data/spec/unit/tent_version_spec.rb +68 -0
- data/tentd.gemspec +36 -0
- metadata +435 -0
@@ -0,0 +1,78 @@
|
|
1
|
+
module TentD
|
2
|
+
class API
|
3
|
+
class Profile
|
4
|
+
include Router
|
5
|
+
|
6
|
+
class AuthorizeWrite < Middleware
|
7
|
+
def action(env)
|
8
|
+
authorize_env!(env, :write_profile)
|
9
|
+
env
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Get < Middleware
|
14
|
+
def action(env)
|
15
|
+
env.response = Model::ProfileInfo.get_profile(env.authorized_scopes, env.current_auth)
|
16
|
+
env
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Update < Middleware
|
21
|
+
def action(env)
|
22
|
+
data = env.params.data
|
23
|
+
type = URI.unescape(env.params.type_url)
|
24
|
+
raise Unauthorized unless ['all', type].find { |t| env.current_auth.profile_info_types.include?(t) }
|
25
|
+
env.updated_info = Model::ProfileInfo.update_profile(type, data)
|
26
|
+
env
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Patch < Middleware
|
31
|
+
def action(env)
|
32
|
+
diff_array = env.params[:data]
|
33
|
+
profile_hash = env.delete(:response)
|
34
|
+
new_profile_hash = Marshal.load(Marshal.dump(profile_hash)).to_hash # equivalent of recursive dup
|
35
|
+
JsonPatch.merge(new_profile_hash, diff_array)
|
36
|
+
if new_profile_hash != profile_hash
|
37
|
+
env.updated_info = new_profile_hash.map do |type, data|
|
38
|
+
Model::ProfileInfo.update_profile(type, data)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
env
|
42
|
+
rescue JsonPatch::ObjectNotFound, JsonPatch::ObjectExists => e
|
43
|
+
env['response.status'] = 422
|
44
|
+
env
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class Notify < Middleware
|
49
|
+
def action(env)
|
50
|
+
return env unless env.updated_info
|
51
|
+
Array(env.updated_info).each do |info|
|
52
|
+
Notifications.profile_info_update(:profile_info_id => info.id)
|
53
|
+
end
|
54
|
+
env
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
get '/profile' do |b|
|
59
|
+
b.use Get
|
60
|
+
end
|
61
|
+
|
62
|
+
put '/profile/:type_url' do |b|
|
63
|
+
b.use AuthorizeWrite
|
64
|
+
b.use Update
|
65
|
+
b.use Get
|
66
|
+
b.use Notify
|
67
|
+
end
|
68
|
+
|
69
|
+
patch '/profile' do |b|
|
70
|
+
b.use AuthorizeWrite
|
71
|
+
b.use Get
|
72
|
+
b.use Patch
|
73
|
+
b.use Get
|
74
|
+
b.use Notify
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'rack/mount'
|
2
|
+
require 'rack/head'
|
3
|
+
|
4
|
+
class Rack::Mount::RouteSet
|
5
|
+
def merge_routes(routes)
|
6
|
+
routes.each { |r| merge_route(r) }
|
7
|
+
rehash
|
8
|
+
end
|
9
|
+
|
10
|
+
def merge_route(route)
|
11
|
+
@routes << route
|
12
|
+
|
13
|
+
@recognition_key_analyzer << route.conditions
|
14
|
+
|
15
|
+
@named_routes[route.name] = route if route.name
|
16
|
+
@generation_route_keys << route.generation_keys
|
17
|
+
|
18
|
+
expire!
|
19
|
+
route
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module TentD
|
24
|
+
class API
|
25
|
+
module Router
|
26
|
+
autoload :ExtractParams, 'tentd/api/router/extract_params'
|
27
|
+
autoload :SerializeResponse, 'tentd/api/router/serialize_response'
|
28
|
+
autoload :CachingHeaders, 'tentd/api/router/caching_headers'
|
29
|
+
|
30
|
+
def self.included(base)
|
31
|
+
base.extend(ClassMethods)
|
32
|
+
end
|
33
|
+
|
34
|
+
def call(env)
|
35
|
+
self.class.routes.call(env)
|
36
|
+
end
|
37
|
+
|
38
|
+
module ClassMethods
|
39
|
+
def mount(klass)
|
40
|
+
routes.merge_routes klass.routes.instance_variable_get("@routes")
|
41
|
+
end
|
42
|
+
|
43
|
+
def routes
|
44
|
+
@routes ||= Rack::Mount::RouteSet.new
|
45
|
+
end
|
46
|
+
|
47
|
+
#### This section heavily "inspired" by sinatra
|
48
|
+
|
49
|
+
# Defining a `GET` handler also automatically defines
|
50
|
+
# a `HEAD` handler.
|
51
|
+
def get(path, opts={}, &block)
|
52
|
+
route('GET', path, opts, &block)
|
53
|
+
route('HEAD', path, opts, &block)
|
54
|
+
end
|
55
|
+
|
56
|
+
def put(path, opts={}, &bk) route 'PUT', path, opts, &bk end
|
57
|
+
def post(path, opts={}, &bk) route 'POST', path, opts, &bk end
|
58
|
+
def delete(path, opts={}, &bk) route 'DELETE', path, opts, &bk end
|
59
|
+
def head(path, opts={}, &bk) route 'HEAD', path, opts, &bk end
|
60
|
+
def options(path, opts={}, &bk) route 'OPTIONS', path, opts, &bk end
|
61
|
+
def patch(path, opts={}, &bk) route 'PATCH', path, opts, &bk end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def route(verb, path, options={}, &block)
|
66
|
+
path, params = compile_path(path)
|
67
|
+
|
68
|
+
return if route_exists?(verb, path)
|
69
|
+
|
70
|
+
builder = Rack::Builder.new(SerializeResponse.new)
|
71
|
+
builder.use(Rack::Head)
|
72
|
+
builder.use(UserLookup)
|
73
|
+
builder.use(AuthenticationLookup)
|
74
|
+
builder.use(AuthenticationVerification)
|
75
|
+
builder.use(AuthenticationFinalize)
|
76
|
+
builder.use(ExtractParams, path, params)
|
77
|
+
builder.use(Authorization)
|
78
|
+
block.call(builder)
|
79
|
+
builder.use(CachingHeaders)
|
80
|
+
|
81
|
+
routes.add_route(builder.to_app, :request_method => verb, :path_info => path)
|
82
|
+
routes.rehash
|
83
|
+
end
|
84
|
+
|
85
|
+
def route_exists?(verb, path)
|
86
|
+
@added_routes ||= []
|
87
|
+
return true if @added_routes.include?("#{verb}#{path}")
|
88
|
+
@added_routes << "#{verb}#{path}"
|
89
|
+
false
|
90
|
+
end
|
91
|
+
|
92
|
+
def compile_path(path)
|
93
|
+
keys = []
|
94
|
+
if path.respond_to? :to_str
|
95
|
+
ignore = ""
|
96
|
+
pattern = path.to_str.gsub(/[^\?\%\\\/\:\*\w]/) do |c|
|
97
|
+
ignore << escaped(c).join if c.match(/[\.@]/)
|
98
|
+
encoded(c)
|
99
|
+
end
|
100
|
+
pattern.gsub!(/((:\w+)|\*)/) do |match|
|
101
|
+
if match == "*"
|
102
|
+
keys << 'splat'
|
103
|
+
"(.*?)"
|
104
|
+
else
|
105
|
+
keys << $2[1..-1]
|
106
|
+
"([^#{ignore}/?#]+)"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
[/\A#{pattern}\z/, keys]
|
110
|
+
elsif path.respond_to?(:keys) && path.respond_to?(:match)
|
111
|
+
[path, path.keys]
|
112
|
+
elsif path.respond_to?(:names) && path.respond_to?(:match)
|
113
|
+
[path, path.names]
|
114
|
+
elsif path.respond_to? :match
|
115
|
+
[path, keys]
|
116
|
+
else
|
117
|
+
raise TypeError, path
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module TentD
|
4
|
+
class API
|
5
|
+
module Router
|
6
|
+
class CachingHeaders
|
7
|
+
def initialize(app)
|
8
|
+
@app = app
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
return @app.call(env) unless %w(GET HEAD).include?(env['REQUEST_METHOD'])
|
13
|
+
last_modified_at = last_modified(env.response)
|
14
|
+
if_modified_since = env['HTTP_IF_MODIFIED_SINCE']
|
15
|
+
if if_modified_since && Time.httpdate(if_modified_since) >= last_modified_at
|
16
|
+
return [304, {}, nil]
|
17
|
+
end
|
18
|
+
status, headers, body = @app.call(env)
|
19
|
+
headers['Last-Modified'] ||= last_modified_at.httpdate if last_modified_at
|
20
|
+
headers['Cache-Control'] ||= cache_control(env.response) if cache_control(env.response)
|
21
|
+
[status, headers, body]
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def last_modified(object)
|
27
|
+
if object.respond_to?(:updated_at)
|
28
|
+
object.updated_at
|
29
|
+
elsif object.kind_of?(Enumerable) && object.first.respond_to?(:updated_at)
|
30
|
+
object.map { |o| o.updated_at }.sort.last
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def cache_control(object)
|
35
|
+
if object.respond_to?(:public) || object.respond_to?(:permissions)
|
36
|
+
public?(object) ? 'public' : 'private'
|
37
|
+
elsif object.kind_of?(Enumerable) && (object.first.respond_to?(:public) || object.first.kind_of?(Hash) && object.first['permissions'])
|
38
|
+
object.map { |o| public?(o) }.uniq == [true] ? 'public' : 'private'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def public?(object)
|
43
|
+
object.respond_to?(:public) && object.public ||
|
44
|
+
object.kind_of?(Hash) && (object['public'] || object['permissions'] && object['permissions']['public'])
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module TentD
|
2
|
+
class API
|
3
|
+
module Router
|
4
|
+
class ExtractParams
|
5
|
+
attr_accessor :pattern, :keys
|
6
|
+
|
7
|
+
def initialize(app, pattern, keys)
|
8
|
+
@app, @pattern, @keys = app, pattern, keys
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
add_request(env)
|
13
|
+
extract_params(env)
|
14
|
+
env['tent.entity'] ||= ENV['TENT_ENTITY']
|
15
|
+
@app.call(env)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def add_request(env)
|
21
|
+
env['request'] = Rack::Request.new(env)
|
22
|
+
end
|
23
|
+
|
24
|
+
def extract_params(env)
|
25
|
+
route = env[Rack::Mount::Prefix::KEY]
|
26
|
+
route = '/' if route.empty?
|
27
|
+
return unless match = pattern.match(route)
|
28
|
+
values = match.captures.to_a.map { |v| URI.decode_www_form_component(v) if v }
|
29
|
+
|
30
|
+
params = env['request'].params.dup
|
31
|
+
|
32
|
+
if values.any?
|
33
|
+
params.merge!('captures' => values)
|
34
|
+
keys.zip(values) { |k,v| Array === params[k] ? params[k] << v : params[k] = v if v }
|
35
|
+
end
|
36
|
+
|
37
|
+
begin
|
38
|
+
if env['CONTENT_TYPE'] =~ /\bjson\Z/
|
39
|
+
params['data'] = env['data'] || JSON.parse(env['rack.input'].read)
|
40
|
+
elsif env['CONTENT_TYPE'] =~ /\Amultipart/
|
41
|
+
key, data = params.find { |k,p| p[:type] == MEDIA_TYPE }
|
42
|
+
params.delete(key)
|
43
|
+
params['data'] = JSON.parse(data[:tempfile].read)
|
44
|
+
params['attachments'] = get_attachments(params)
|
45
|
+
end
|
46
|
+
rescue JSON::ParserError
|
47
|
+
end
|
48
|
+
|
49
|
+
env['params'] = indifferent_params(params)
|
50
|
+
end
|
51
|
+
|
52
|
+
def get_attachments(params)
|
53
|
+
params.inject([]) { |a,(key,value)|
|
54
|
+
if attachment?(value)
|
55
|
+
a << value.merge(:name => key)
|
56
|
+
elsif value.kind_of?(Hash)
|
57
|
+
a += value.select { |k,v| attachment?(v) }.map { |k,v| v.merge(:name => key) }
|
58
|
+
end
|
59
|
+
a
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
def attachment?(v)
|
64
|
+
v.kind_of?(Hash) && v[:tempfile].kind_of?(Tempfile)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Enable string or symbol key access to the nested params hash.
|
68
|
+
def indifferent_params(object)
|
69
|
+
case object
|
70
|
+
when Hash
|
71
|
+
new_hash = indifferent_hash
|
72
|
+
object.each { |key, value| new_hash[key] = indifferent_params(value) }
|
73
|
+
new_hash
|
74
|
+
when Array
|
75
|
+
object.map { |item| indifferent_params(item) }
|
76
|
+
else
|
77
|
+
object
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Creates a Hash with indifferent access.
|
82
|
+
def indifferent_hash
|
83
|
+
Hash.new {|hash,key| hash[key.to_s] if Symbol === key }
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module TentD
|
4
|
+
class API
|
5
|
+
module Router
|
6
|
+
class SerializeResponse
|
7
|
+
def call(env)
|
8
|
+
response = if env.response
|
9
|
+
env.response.kind_of?(String) ? env.response : env.response.to_json(serialization_options(env))
|
10
|
+
end
|
11
|
+
status = env['response.status'] || (response ? 200 : 404)
|
12
|
+
headers = if env['response.type'] || status == 200 && response && !response.empty?
|
13
|
+
{ 'Content-Type' => env['response.type'] || MEDIA_TYPE }
|
14
|
+
else
|
15
|
+
{}
|
16
|
+
end
|
17
|
+
headers.merge!('Access-Control-Allow-Origin' => '*') if env['HTTP_ORIGIN']
|
18
|
+
[status, headers, [response.to_s]]
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def serialization_options(env)
|
24
|
+
{
|
25
|
+
:app => env.current_auth.kind_of?(Model::AppAuthorization),
|
26
|
+
:authorization_token => env.authorized_scopes.include?(:read_apps),
|
27
|
+
:permissions => env.authorized_scopes.include?(:read_permissions),
|
28
|
+
:groups => env.authorized_scopes.include?(:read_groups),
|
29
|
+
:mac => env.authorized_scopes.include?(:read_secrets),
|
30
|
+
:self => env.authorized_scopes.include?(:self),
|
31
|
+
:auth_token => env.authorized_scopes.include?(:authorization_token),
|
32
|
+
:view => env.params.view
|
33
|
+
}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# Taken from rails
|
2
|
+
class Hash
|
3
|
+
# Slice a hash to include only the given keys. This is useful for
|
4
|
+
# limiting an options hash to valid keys before passing to a method:
|
5
|
+
#
|
6
|
+
# def search(criteria = {})
|
7
|
+
# assert_valid_keys(:mass, :velocity, :time)
|
8
|
+
# end
|
9
|
+
#
|
10
|
+
# search(options.slice(:mass, :velocity, :time))
|
11
|
+
#
|
12
|
+
# If you have an array of keys you want to limit to, you should splat them:
|
13
|
+
#
|
14
|
+
# valid_keys = [:mass, :velocity, :time]
|
15
|
+
# search(options.slice(*valid_keys))
|
16
|
+
def slice(*keys)
|
17
|
+
keys.each_with_object(self.class.new) { |k, hash| hash[k] = self[k] if has_key?(k) }
|
18
|
+
end
|
19
|
+
|
20
|
+
# Replaces the hash with only the given keys.
|
21
|
+
# Returns a hash containing the removed key/value pairs.
|
22
|
+
# {:a => 1, :b => 2, :c => 3, :d => 4}.slice!(:a, :b) # => {:c => 3, :d => 4}
|
23
|
+
def slice!(*keys)
|
24
|
+
omit = slice(*self.keys - keys)
|
25
|
+
hash = slice(*keys)
|
26
|
+
replace(hash)
|
27
|
+
omit
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module DataMapper
|
2
|
+
class Property
|
3
|
+
# Implements flat postgres string arrays
|
4
|
+
class Array < DataMapper::Property::Text
|
5
|
+
def custom?
|
6
|
+
true
|
7
|
+
end
|
8
|
+
|
9
|
+
def load(value)
|
10
|
+
return value if value.kind_of? ::Array
|
11
|
+
value[1..-2].split(',').map { |v| v[0,1] == '"' ? v[1..-2] : v } unless value.nil?
|
12
|
+
end
|
13
|
+
|
14
|
+
def dump(value)
|
15
|
+
"{#{value.map(&:to_s).map(&:inspect).join(',')}}" unless value.nil?
|
16
|
+
end
|
17
|
+
|
18
|
+
def typecast(value)
|
19
|
+
load(value)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|