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.
Files changed (101) hide show
  1. data/.gitignore +18 -0
  2. data/.rspec +1 -0
  3. data/.travis.yml +8 -0
  4. data/Gemfile +9 -0
  5. data/Guardfile +6 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +49 -0
  8. data/Rakefile +8 -0
  9. data/bin/tent-server +3 -0
  10. data/lib/tentd.rb +31 -0
  11. data/lib/tentd/api.rb +58 -0
  12. data/lib/tentd/api/apps.rb +196 -0
  13. data/lib/tentd/api/authentication_finalize.rb +12 -0
  14. data/lib/tentd/api/authentication_lookup.rb +27 -0
  15. data/lib/tentd/api/authentication_verification.rb +50 -0
  16. data/lib/tentd/api/authorizable.rb +21 -0
  17. data/lib/tentd/api/authorization.rb +14 -0
  18. data/lib/tentd/api/core_profile_data.rb +45 -0
  19. data/lib/tentd/api/followers.rb +218 -0
  20. data/lib/tentd/api/followings.rb +241 -0
  21. data/lib/tentd/api/groups.rb +161 -0
  22. data/lib/tentd/api/middleware.rb +32 -0
  23. data/lib/tentd/api/posts.rb +373 -0
  24. data/lib/tentd/api/profile.rb +78 -0
  25. data/lib/tentd/api/router.rb +123 -0
  26. data/lib/tentd/api/router/caching_headers.rb +49 -0
  27. data/lib/tentd/api/router/extract_params.rb +88 -0
  28. data/lib/tentd/api/router/serialize_response.rb +38 -0
  29. data/lib/tentd/api/user_lookup.rb +10 -0
  30. data/lib/tentd/core_ext/hash/slice.rb +29 -0
  31. data/lib/tentd/datamapper/array_property.rb +23 -0
  32. data/lib/tentd/datamapper/query.rb +19 -0
  33. data/lib/tentd/json_patch.rb +181 -0
  34. data/lib/tentd/model.rb +30 -0
  35. data/lib/tentd/model/app.rb +68 -0
  36. data/lib/tentd/model/app_authorization.rb +113 -0
  37. data/lib/tentd/model/follower.rb +105 -0
  38. data/lib/tentd/model/following.rb +100 -0
  39. data/lib/tentd/model/group.rb +24 -0
  40. data/lib/tentd/model/mention.rb +19 -0
  41. data/lib/tentd/model/notification_subscription.rb +56 -0
  42. data/lib/tentd/model/permissible.rb +227 -0
  43. data/lib/tentd/model/permission.rb +28 -0
  44. data/lib/tentd/model/post.rb +178 -0
  45. data/lib/tentd/model/post_attachment.rb +27 -0
  46. data/lib/tentd/model/post_version.rb +64 -0
  47. data/lib/tentd/model/profile_info.rb +80 -0
  48. data/lib/tentd/model/random_public_id.rb +46 -0
  49. data/lib/tentd/model/serializable.rb +58 -0
  50. data/lib/tentd/model/type_properties.rb +36 -0
  51. data/lib/tentd/model/user.rb +39 -0
  52. data/lib/tentd/model/user_scoped.rb +14 -0
  53. data/lib/tentd/notifications.rb +13 -0
  54. data/lib/tentd/notifications/girl_friday.rb +30 -0
  55. data/lib/tentd/notifications/sidekiq.rb +50 -0
  56. data/lib/tentd/tent_type.rb +20 -0
  57. data/lib/tentd/tent_version.rb +41 -0
  58. data/lib/tentd/version.rb +3 -0
  59. data/spec/fabricators/app_authorizations_fabricator.rb +5 -0
  60. data/spec/fabricators/apps_fabricator.rb +11 -0
  61. data/spec/fabricators/followers_fabricator.rb +14 -0
  62. data/spec/fabricators/followings_fabricator.rb +17 -0
  63. data/spec/fabricators/groups_fabricator.rb +3 -0
  64. data/spec/fabricators/mentions_fabricator.rb +3 -0
  65. data/spec/fabricators/notification_subscriptions_fabricator.rb +4 -0
  66. data/spec/fabricators/permissions_fabricator.rb +1 -0
  67. data/spec/fabricators/post_attachments_fabricator.rb +8 -0
  68. data/spec/fabricators/post_versions_fabricator.rb +12 -0
  69. data/spec/fabricators/posts_fabricator.rb +12 -0
  70. data/spec/fabricators/profile_infos_fabricator.rb +30 -0
  71. data/spec/integration/api/apps_spec.rb +466 -0
  72. data/spec/integration/api/followers_spec.rb +535 -0
  73. data/spec/integration/api/followings_spec.rb +688 -0
  74. data/spec/integration/api/groups_spec.rb +207 -0
  75. data/spec/integration/api/posts_spec.rb +874 -0
  76. data/spec/integration/api/profile_spec.rb +285 -0
  77. data/spec/integration/api/router_spec.rb +102 -0
  78. data/spec/integration/model/app_authorization_spec.rb +59 -0
  79. data/spec/integration/model/app_spec.rb +63 -0
  80. data/spec/integration/model/follower_spec.rb +344 -0
  81. data/spec/integration/model/following_spec.rb +97 -0
  82. data/spec/integration/model/group_spec.rb +39 -0
  83. data/spec/integration/model/notification_subscription_spec.rb +145 -0
  84. data/spec/integration/model/post_spec.rb +658 -0
  85. data/spec/spec_helper.rb +37 -0
  86. data/spec/support/expect_server.rb +3 -0
  87. data/spec/support/json_request.rb +54 -0
  88. data/spec/support/with_constant.rb +23 -0
  89. data/spec/support/with_warnings.rb +6 -0
  90. data/spec/unit/api/authentication_finalize_spec.rb +45 -0
  91. data/spec/unit/api/authentication_lookup_spec.rb +65 -0
  92. data/spec/unit/api/authentication_verification_spec.rb +50 -0
  93. data/spec/unit/api/authorizable_spec.rb +50 -0
  94. data/spec/unit/api/authorization_spec.rb +44 -0
  95. data/spec/unit/api/caching_headers_spec.rb +121 -0
  96. data/spec/unit/core_profile_data_spec.rb +64 -0
  97. data/spec/unit/json_patch_spec.rb +407 -0
  98. data/spec/unit/tent_type_spec.rb +28 -0
  99. data/spec/unit/tent_version_spec.rb +68 -0
  100. data/tentd.gemspec +36 -0
  101. 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,10 @@
1
+ module TentD
2
+ class API
3
+ class UserLookup < Middleware
4
+ def action(env)
5
+ Model::User.current ||= Model::User.first_or_create
6
+ env
7
+ end
8
+ end
9
+ end
10
+ 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