tentd 0.0.1
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.
- 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
|