croods 0.1.0

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 (65) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +19 -0
  5. data/lib/croods.rb +58 -0
  6. data/lib/croods/action.rb +17 -0
  7. data/lib/croods/api.rb +26 -0
  8. data/lib/croods/api/initial_schema.json +52 -0
  9. data/lib/croods/attribute.rb +15 -0
  10. data/lib/croods/controller.rb +28 -0
  11. data/lib/croods/controller/actions.rb +71 -0
  12. data/lib/croods/controller/already_taken.rb +27 -0
  13. data/lib/croods/controller/authentication.rb +22 -0
  14. data/lib/croods/controller/authorization.rb +31 -0
  15. data/lib/croods/controller/collection.rb +15 -0
  16. data/lib/croods/controller/member.rb +45 -0
  17. data/lib/croods/controller/model.rb +17 -0
  18. data/lib/croods/controller/multi_tenancy.rb +54 -0
  19. data/lib/croods/controller/not_found.rb +22 -0
  20. data/lib/croods/controller/record_invalid.rb +22 -0
  21. data/lib/croods/controller/resource.rb +30 -0
  22. data/lib/croods/middleware.rb +44 -0
  23. data/lib/croods/middleware/base.rb +20 -0
  24. data/lib/croods/middleware/request_validation.rb +10 -0
  25. data/lib/croods/middleware/response_validation.rb +10 -0
  26. data/lib/croods/model.rb +41 -0
  27. data/lib/croods/policy.rb +118 -0
  28. data/lib/croods/policy/scope.rb +131 -0
  29. data/lib/croods/railtie.rb +6 -0
  30. data/lib/croods/resource.rb +39 -0
  31. data/lib/croods/resource/actions.rb +51 -0
  32. data/lib/croods/resource/attributes.rb +53 -0
  33. data/lib/croods/resource/attributes/base.rb +30 -0
  34. data/lib/croods/resource/attributes/request.rb +29 -0
  35. data/lib/croods/resource/attributes/response.rb +13 -0
  36. data/lib/croods/resource/authentication.rb +46 -0
  37. data/lib/croods/resource/authorization.rb +47 -0
  38. data/lib/croods/resource/controller.rb +50 -0
  39. data/lib/croods/resource/filters.rb +32 -0
  40. data/lib/croods/resource/identifier.rb +13 -0
  41. data/lib/croods/resource/json_schema.rb +32 -0
  42. data/lib/croods/resource/json_schema/definition.rb +34 -0
  43. data/lib/croods/resource/json_schema/definitions.rb +31 -0
  44. data/lib/croods/resource/json_schema/initial_schema.json +8 -0
  45. data/lib/croods/resource/json_schema/links.rb +40 -0
  46. data/lib/croods/resource/json_schema/links/collection.rb +70 -0
  47. data/lib/croods/resource/json_schema/links/create.rb +41 -0
  48. data/lib/croods/resource/json_schema/links/destroy.rb +40 -0
  49. data/lib/croods/resource/json_schema/links/index.rb +17 -0
  50. data/lib/croods/resource/json_schema/links/member.rb +40 -0
  51. data/lib/croods/resource/json_schema/links/show.rb +40 -0
  52. data/lib/croods/resource/json_schema/links/update.rb +40 -0
  53. data/lib/croods/resource/json_schema/properties.rb +32 -0
  54. data/lib/croods/resource/json_schema/required.rb +34 -0
  55. data/lib/croods/resource/model.rb +35 -0
  56. data/lib/croods/resource/names.rb +23 -0
  57. data/lib/croods/resource/paths.rb +16 -0
  58. data/lib/croods/resource/policy.rb +51 -0
  59. data/lib/croods/resource/services.rb +18 -0
  60. data/lib/croods/resource/sorting.rb +13 -0
  61. data/lib/croods/routes.rb +46 -0
  62. data/lib/croods/service.rb +28 -0
  63. data/lib/croods/version.rb +5 -0
  64. data/lib/tasks/croods_tasks.rake +4 -0
  65. metadata +316 -0
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Croods
4
+ class Policy
5
+ class Scope
6
+ def initialize(tenant:, user:, scope:)
7
+ self.user = user
8
+ self.scope = scope
9
+ self.tenant = user&.tenant || tenant
10
+ end
11
+
12
+ def resolve
13
+ self.scope = tenant_scope(scope) if Croods.multi_tenancy?
14
+
15
+ return scope if action.public
16
+
17
+ return scope if super?
18
+
19
+ return owner_scope(scope) if owner?
20
+
21
+ scope
22
+ end
23
+
24
+ protected
25
+
26
+ attr_accessor :tenant, :user, :scope
27
+
28
+ def scope_is_the_owner?(scope, target)
29
+ return scope.reflect_on_association(target) unless target == :user
30
+
31
+ scope.resource.user_is_the_owner? &&
32
+ scope.reflect_on_association(target)
33
+ end
34
+
35
+ def reflection_path(scope, target, path = [])
36
+ return path if scope_is_the_owner?(scope, target)
37
+
38
+ associations = scope.reflect_on_all_associations(:belongs_to)
39
+
40
+ return path if associations.empty?
41
+
42
+ associations.each do |association|
43
+ model = association.class_name.constantize
44
+ expanded_path = path + [association]
45
+ association_path = reflection_path(model, target, expanded_path)
46
+ return association_path if association_path != path
47
+ end
48
+
49
+ path
50
+ end
51
+
52
+ def joins(path)
53
+ joins = nil
54
+
55
+ path.reverse.each do |association|
56
+ joins = joins ? { association.name => joins } : association.name
57
+ end
58
+
59
+ joins
60
+ end
61
+
62
+ def immediate_tenant_scope(scope)
63
+ scope.where(Croods.tenant_attribute => tenant.id)
64
+ end
65
+
66
+ def immediate_tenant_scope?(scope)
67
+ scope.has_attribute?(Croods.tenant_attribute)
68
+ end
69
+
70
+ def tenant_scope(scope)
71
+ return immediate_tenant_scope(scope) if immediate_tenant_scope?(scope)
72
+
73
+ path = reflection_path(scope, Croods.multi_tenancy_by)
74
+ return scope if path.empty?
75
+
76
+ scope.joins(joins(path)).where(
77
+ path.last.plural_name => { Croods.tenant_attribute => tenant.id }
78
+ )
79
+ end
80
+
81
+ def user_owner_scope(scope)
82
+ scope.where(id: user.id)
83
+ end
84
+
85
+ def user_owner_scope?(scope)
86
+ scope == User || scope.try(:klass) == User
87
+ end
88
+
89
+ def immediate_owner_scope(scope)
90
+ scope.where(user_id: user.id)
91
+ end
92
+
93
+ def immediate_owner_scope?(scope)
94
+ scope.resource.user_is_the_owner? && scope.has_attribute?(:user_id)
95
+ end
96
+
97
+ def owner_scope(scope)
98
+ return user_owner_scope(scope) if user_owner_scope?(scope)
99
+
100
+ return immediate_owner_scope(scope) if immediate_owner_scope?(scope)
101
+
102
+ path = reflection_path(scope, :user)
103
+ return scope if path.empty?
104
+
105
+ scope.joins(joins(path)).where(
106
+ path.last.plural_name => { user_id: user.id }
107
+ )
108
+ end
109
+
110
+ def super?
111
+ super_roles.each do |role|
112
+ return true if user&.send("#{role}?")
113
+ end
114
+
115
+ false
116
+ end
117
+
118
+ def roles
119
+ action.roles || DEFAULT_ROLES
120
+ end
121
+
122
+ def super_roles
123
+ roles.reject { |role| role == :owner }
124
+ end
125
+
126
+ def owner?
127
+ roles.include?(:owner)
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Croods
4
+ class Railtie < ::Rails::Railtie
5
+ end
6
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'resource/names'
4
+ require_relative 'resource/paths'
5
+ require_relative 'resource/identifier'
6
+ require_relative 'resource/model'
7
+ require_relative 'resource/policy'
8
+ require_relative 'resource/controller'
9
+ require_relative 'resource/actions'
10
+ require_relative 'resource/attributes'
11
+ require_relative 'resource/json_schema'
12
+ require_relative 'resource/authentication'
13
+ require_relative 'resource/authorization'
14
+ require_relative 'resource/filters'
15
+ require_relative 'resource/sorting'
16
+ require_relative 'resource/services'
17
+
18
+ module Croods
19
+ module Resource
20
+ extend ActiveSupport::Concern
21
+
22
+ class_methods do
23
+ include Names
24
+ include Paths
25
+ include Identifier
26
+ include Model
27
+ include Policy
28
+ include Controller
29
+ include Actions
30
+ include Attributes
31
+ include JsonSchema
32
+ include Authentication
33
+ include Authorization
34
+ include Filters
35
+ include Sorting
36
+ include Services
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Croods
4
+ module Resource
5
+ module Actions
6
+ DEFAULT_ACTIONS = %i[index create update destroy show].freeze
7
+
8
+ def default_actions
9
+ DEFAULT_ACTIONS.map do |name|
10
+ Croods::Action.new name
11
+ end
12
+ end
13
+
14
+ def actions(*names)
15
+ return filtered_actions if names.empty?
16
+
17
+ @actions = names.map do |name|
18
+ Croods::Action.new name
19
+ end
20
+ end
21
+
22
+ def filtered_actions
23
+ @actions ||= default_actions
24
+
25
+ @actions.reject { |action| ignored_actions.include?(action.name) }
26
+ end
27
+
28
+ def add_action(name, method: :get, on: :member, &block)
29
+ additional_actions << Action.new(
30
+ name, method: method, on: on, block: block
31
+ )
32
+ end
33
+
34
+ def additional_actions
35
+ @additional_actions ||= []
36
+ end
37
+
38
+ def remove_actions(*names)
39
+ names.each do |name|
40
+ ignored_actions << name.to_sym
41
+ end
42
+ end
43
+
44
+ def ignored_actions
45
+ @ignored_actions ||= []
46
+ end
47
+
48
+ alias remove_action remove_actions
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'attributes/base'
4
+ require_relative 'attributes/request'
5
+ require_relative 'attributes/response'
6
+
7
+ module Croods
8
+ module Resource
9
+ module Attributes
10
+ include Base
11
+
12
+ def request(&block)
13
+ request_instance.instance_eval(&block)
14
+ end
15
+
16
+ def response(&block)
17
+ response_instance.instance_eval(&block)
18
+ end
19
+
20
+ def request_instance
21
+ @request_instance ||= Request.new(ignore_user: user_is_the_owner?)
22
+ end
23
+
24
+ def response_instance
25
+ @response_instance ||= Response.new
26
+ end
27
+
28
+ def merged_attributes(type, hash = nil)
29
+ (hash || attributes)
30
+ .merge(type.additional_attributes)
31
+ .reject { |name| type.ignored_attributes.include?(name) }
32
+ end
33
+
34
+ def request_attributes
35
+ merged_attributes(request_instance)
36
+ end
37
+
38
+ def response_attributes
39
+ merged_attributes(response_instance)
40
+ end
41
+
42
+ def attributes
43
+ merged_attributes(self, model.columns_hash)
44
+ end
45
+
46
+ def definitions
47
+ attributes
48
+ .merge(request_instance.additional_attributes)
49
+ .merge(response_instance.additional_attributes)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Croods
4
+ module Resource
5
+ module Attributes
6
+ module Base
7
+ def add_attribute(name, type, **options)
8
+ attribute = Croods::Attribute.new(name, type, **options)
9
+ additional_attributes[name.to_s] = attribute
10
+ end
11
+
12
+ def additional_attributes
13
+ @additional_attributes ||= {}
14
+ end
15
+
16
+ def remove_attributes(*names)
17
+ names.each do |name|
18
+ ignored_attributes << name.to_s
19
+ end
20
+ end
21
+
22
+ def ignored_attributes
23
+ @ignored_attributes ||= []
24
+ end
25
+
26
+ alias remove_attribute remove_attributes
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Croods
6
+ module Resource
7
+ module Attributes
8
+ class Request
9
+ include Base
10
+
11
+ attr_accessor :ignore_user
12
+
13
+ def initialize(ignore_user:)
14
+ self.ignore_user = ignore_user
15
+ end
16
+
17
+ def ignored_attributes
18
+ @ignored_attributes ||= default_ignored_attributes
19
+ end
20
+
21
+ def default_ignored_attributes
22
+ return [] unless ignore_user
23
+
24
+ ['user_id']
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Croods
6
+ module Resource
7
+ module Attributes
8
+ class Response
9
+ include Base
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Croods
4
+ module Resource
5
+ module Authentication
6
+ OPTIONS = %i[
7
+ database_authenticatable registerable recoverable rememberable
8
+ trackable validatable
9
+ ].freeze
10
+
11
+ ATTRIBUTES = %i[
12
+ allow_password_change confirmation_sent_at confirmation_token
13
+ encrypted_password confirmed_at provider uid remember_created_at
14
+ reset_password_sent_at reset_password_token tokens unconfirmed_email
15
+ current_sign_in_at current_sign_in_ip last_sign_in_at last_sign_in_ip
16
+ sign_in_count
17
+ ].freeze
18
+
19
+ def use_for_authentication!(*options)
20
+ add_model_authentication(*options)
21
+
22
+ remove_attributes(*ATTRIBUTES)
23
+ remove_attribute(Croods.tenant_attribute) if Croods.multi_tenancy?
24
+
25
+ request do
26
+ add_attribute :password, :string, null: false
27
+ end
28
+ end
29
+
30
+ def add_model_authentication(*options)
31
+ extend_model do
32
+ before_create do
33
+ self.uid = email unless uid.present?
34
+ end
35
+
36
+ extend Devise::Models
37
+
38
+ devise_options = options.empty? ? OPTIONS : options
39
+ devise(*devise_options)
40
+
41
+ include DeviseTokenAuth::Concerns::User
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Croods
4
+ module Resource
5
+ module Authorization
6
+ def authorize(*roles, on: nil)
7
+ return if roles.empty?
8
+
9
+ on = [on] if on&.is_a?(Symbol)
10
+
11
+ actions.each do |action|
12
+ next if on && !on.include?(action.name)
13
+
14
+ action.roles = roles
15
+ end
16
+ end
17
+
18
+ def public_actions(*names)
19
+ return unless names
20
+
21
+ names = [names] if names&.is_a?(Symbol)
22
+
23
+ extend_controller do
24
+ skip_before_action :authenticate_user!, only: names
25
+ end
26
+
27
+ actions.each do |action|
28
+ next unless names.include?(action.name)
29
+
30
+ action.public = true
31
+ end
32
+ end
33
+
34
+ alias public_action public_actions
35
+
36
+ def user_is_not_the_owner!
37
+ @user_is_the_owner = false
38
+ end
39
+
40
+ def user_is_the_owner?
41
+ return @user_is_the_owner unless @user_is_the_owner.nil?
42
+
43
+ @user_is_the_owner = true
44
+ end
45
+ end
46
+ end
47
+ end