rhino_project_core 0.20.0.alpha.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (134) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +28 -0
  4. data/Rakefile +35 -0
  5. data/app/assets/stripe_flow.png +0 -0
  6. data/app/controllers/concerns/rhino/authenticated.rb +18 -0
  7. data/app/controllers/concerns/rhino/error_handling.rb +60 -0
  8. data/app/controllers/concerns/rhino/paper_trail_whodunnit.rb +11 -0
  9. data/app/controllers/concerns/rhino/permit.rb +38 -0
  10. data/app/controllers/concerns/rhino/set_current_user.rb +13 -0
  11. data/app/controllers/rhino/account_controller.rb +34 -0
  12. data/app/controllers/rhino/active_model_extension_controller.rb +52 -0
  13. data/app/controllers/rhino/base_controller.rb +23 -0
  14. data/app/controllers/rhino/crud_controller.rb +57 -0
  15. data/app/controllers/rhino/simple_controller.rb +13 -0
  16. data/app/controllers/rhino/simple_stream_controller.rb +12 -0
  17. data/app/helpers/rhino/omniauth_helper.rb +67 -0
  18. data/app/helpers/rhino/policy_helper.rb +42 -0
  19. data/app/helpers/rhino/segment_helper.rb +62 -0
  20. data/app/models/rhino/account.rb +13 -0
  21. data/app/models/rhino/current.rb +7 -0
  22. data/app/models/rhino/user.rb +44 -0
  23. data/app/overrides/active_record/autosave_association_override.rb +18 -0
  24. data/app/overrides/active_record/delegated_type_override.rb +14 -0
  25. data/app/overrides/activestorage/direct_uploads_controller_override.rb +23 -0
  26. data/app/overrides/activestorage/redirect_controller_override.rb +21 -0
  27. data/app/overrides/activestorage/redirect_representation_controller_override.rb +21 -0
  28. data/app/overrides/devise_token_auth/confirmations_controller_override.rb +14 -0
  29. data/app/overrides/devise_token_auth/omniauth_callbacks_controller_override.rb +45 -0
  30. data/app/overrides/devise_token_auth/passwords_controller_override.rb +9 -0
  31. data/app/overrides/devise_token_auth/registrations_controller_override.rb +20 -0
  32. data/app/overrides/devise_token_auth/sessions_controller_override.rb +26 -0
  33. data/app/overrides/devise_token_auth/token_validations_controller_override.rb +18 -0
  34. data/app/policies/rhino/account_policy.rb +27 -0
  35. data/app/policies/rhino/active_storage_attachment_policy.rb +16 -0
  36. data/app/policies/rhino/admin_policy.rb +20 -0
  37. data/app/policies/rhino/base_policy.rb +72 -0
  38. data/app/policies/rhino/crud_policy.rb +109 -0
  39. data/app/policies/rhino/editor_policy.rb +12 -0
  40. data/app/policies/rhino/global_policy.rb +8 -0
  41. data/app/policies/rhino/resource_info_policy.rb +9 -0
  42. data/app/policies/rhino/user_policy.rb +20 -0
  43. data/app/policies/rhino/viewer_policy.rb +19 -0
  44. data/app/resources/rhino/info_graph.rb +41 -0
  45. data/app/resources/rhino/open_api_info.rb +108 -0
  46. data/config/routes.rb +19 -0
  47. data/db/migrate/20180101000000_devise_token_auth_create_users.rb +54 -0
  48. data/db/migrate/20180622142754_add_allow_change_password_to_users.rb +5 -0
  49. data/db/migrate/20191217010224_add_approved_to_users.rb +7 -0
  50. data/db/migrate/20200503182019_change_tokens_to_json_b.rb +9 -0
  51. data/lib/commands/rhino/module/coverage_command.rb +44 -0
  52. data/lib/commands/rhino/module/dummy_command.rb +43 -0
  53. data/lib/commands/rhino/module/new_command.rb +34 -0
  54. data/lib/commands/rhino/module/rails_command.rb +43 -0
  55. data/lib/commands/rhino/module/test_command.rb +43 -0
  56. data/lib/generators/rhino/dev/setup/setup_generator.rb +199 -0
  57. data/lib/generators/rhino/dev/setup/templates/env.client.tt +4 -0
  58. data/lib/generators/rhino/dev/setup/templates/env.root.tt +1 -0
  59. data/lib/generators/rhino/dev/setup/templates/env.server.tt +35 -0
  60. data/lib/generators/rhino/dev/setup/templates/prepare-commit-msg +17 -0
  61. data/lib/generators/rhino/install/install_generator.rb +24 -0
  62. data/lib/generators/rhino/install/templates/account.rb +4 -0
  63. data/lib/generators/rhino/install/templates/rhino.rb +24 -0
  64. data/lib/generators/rhino/install/templates/user.rb +4 -0
  65. data/lib/generators/rhino/model/model_generator.rb +96 -0
  66. data/lib/generators/rhino/module/USAGE +6 -0
  67. data/lib/generators/rhino/module/module_generator.rb +92 -0
  68. data/lib/generators/rhino/module/templates/%name%.gemspec.tt +24 -0
  69. data/lib/generators/rhino/module/templates/lib/%namespaced_name%/engine.rb.tt +18 -0
  70. data/lib/generators/rhino/module/templates/lib/generators/%namespaced_name%/install/install_generator.rb.tt +12 -0
  71. data/lib/generators/rhino/module/templates/lib/tasks/%namespaced_name%_tasks.rake.tt +13 -0
  72. data/lib/generators/rhino/module/templates/test/dummy/app/models/user.rb +4 -0
  73. data/lib/generators/rhino/module/templates/test/dummy/config/database.yml +25 -0
  74. data/lib/generators/rhino/module/templates/test/dummy/config/initializers/devise.rb +311 -0
  75. data/lib/generators/rhino/module/templates/test/dummy/config/initializers/devise_token_auth.rb +71 -0
  76. data/lib/generators/rhino/module/templates/test/test_helper.rb +54 -0
  77. data/lib/generators/rhino/policy/policy_generator.rb +33 -0
  78. data/lib/generators/rhino/policy/templates/policy.rb.tt +46 -0
  79. data/lib/generators/test_unit/rhino_policy_generator.rb +13 -0
  80. data/lib/generators/test_unit/templates/policy_test.rb.tt +57 -0
  81. data/lib/rhino/engine.rb +166 -0
  82. data/lib/rhino/omniauth/strategies/azure_o_auth2.rb +16 -0
  83. data/lib/rhino/resource/active_model_extension/backing_store/google_sheet.rb +89 -0
  84. data/lib/rhino/resource/active_model_extension/backing_store.rb +33 -0
  85. data/lib/rhino/resource/active_model_extension/describe.rb +38 -0
  86. data/lib/rhino/resource/active_model_extension/params.rb +70 -0
  87. data/lib/rhino/resource/active_model_extension/properties.rb +231 -0
  88. data/lib/rhino/resource/active_model_extension/reference.rb +50 -0
  89. data/lib/rhino/resource/active_model_extension/routing.rb +15 -0
  90. data/lib/rhino/resource/active_model_extension/serialization.rb +16 -0
  91. data/lib/rhino/resource/active_model_extension.rb +38 -0
  92. data/lib/rhino/resource/active_record_extension/describe.rb +44 -0
  93. data/lib/rhino/resource/active_record_extension/params.rb +213 -0
  94. data/lib/rhino/resource/active_record_extension/properties.rb +85 -0
  95. data/lib/rhino/resource/active_record_extension/properties_describe.rb +228 -0
  96. data/lib/rhino/resource/active_record_extension/reference.rb +50 -0
  97. data/lib/rhino/resource/active_record_extension/routing.rb +21 -0
  98. data/lib/rhino/resource/active_record_extension/search.rb +23 -0
  99. data/lib/rhino/resource/active_record_extension/serialization.rb +16 -0
  100. data/lib/rhino/resource/active_record_extension/super_admin.rb +25 -0
  101. data/lib/rhino/resource/active_record_extension.rb +32 -0
  102. data/lib/rhino/resource/active_record_tree.rb +50 -0
  103. data/lib/rhino/resource/active_storage_extension.rb +41 -0
  104. data/lib/rhino/resource/describe.rb +19 -0
  105. data/lib/rhino/resource/owner.rb +172 -0
  106. data/lib/rhino/resource/params.rb +31 -0
  107. data/lib/rhino/resource/properties.rb +192 -0
  108. data/lib/rhino/resource/reference.rb +29 -0
  109. data/lib/rhino/resource/routing.rb +107 -0
  110. data/lib/rhino/resource/serialization.rb +13 -0
  111. data/lib/rhino/resource/sieves.rb +36 -0
  112. data/lib/rhino/resource.rb +55 -0
  113. data/lib/rhino/sieve/filter.rb +149 -0
  114. data/lib/rhino/sieve/geospatial.rb +45 -0
  115. data/lib/rhino/sieve/helpers.rb +11 -0
  116. data/lib/rhino/sieve/limit.rb +20 -0
  117. data/lib/rhino/sieve/offset.rb +16 -0
  118. data/lib/rhino/sieve/order.rb +143 -0
  119. data/lib/rhino/sieve/search.rb +20 -0
  120. data/lib/rhino/sieve.rb +159 -0
  121. data/lib/rhino/test_case/controller.rb +145 -0
  122. data/lib/rhino/test_case/model.rb +86 -0
  123. data/lib/rhino/test_case/override.rb +19 -0
  124. data/lib/rhino/test_case/policy.rb +76 -0
  125. data/lib/rhino/test_case.rb +11 -0
  126. data/lib/rhino/version.rb +17 -0
  127. data/lib/rhino_project_core.rb +131 -0
  128. data/lib/tasks/rhino.rake +24 -0
  129. data/lib/tasks/rhino_dev.rake +17 -0
  130. data/lib/validators/country_validator.rb +11 -0
  131. data/lib/validators/email_validator.rb +8 -0
  132. data/lib/validators/ipv4_validator.rb +10 -0
  133. data/lib/validators/mac_address_validator.rb +9 -0
  134. metadata +531 -0
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rhino
4
+ module Sieve
5
+ class Filter # rubocop:disable Metrics/ClassLength
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ # filter[blog_id]=1
11
+ def resolve(scope, params)
12
+ return @app.resolve(scope, params) unless params.key?(:filter)
13
+
14
+ filter = params[:filter].permit!.to_h
15
+ scope = scope.joins(get_joins(scope.klass, filter))
16
+ query = apply_filters(scope, scope.klass, filter).distinct(:id)
17
+ @app.resolve(query, params)
18
+ end
19
+
20
+ private
21
+ def get_joins_hash(base, filter)
22
+ res = []
23
+ filter.each do |key, val|
24
+ value_is_hash = val.is_a? Hash
25
+ # only consider a key for the join clause if it is a relationship a.k.a reflection
26
+ key_is_reflection = base.reflections.key? key
27
+
28
+ next unless value_is_hash && key_is_reflection
29
+
30
+ # in the next recursion, the associations will have to be checked against this current key, not the initial model
31
+ reflection_model = base.reflections[key.to_s].klass
32
+ res << { key => get_joins_hash(reflection_model, val) }
33
+ end
34
+ res
35
+ end
36
+
37
+ def get_joins(base, filter)
38
+ joins_hash = get_joins_hash(base, filter)
39
+ result = []
40
+ joins_hash.each do |entry|
41
+ entry.each do |key, value|
42
+ result << ArelHelpers.join_association(base, { key => value }, Arel::Nodes::OuterJoin, {})
43
+ end
44
+ end
45
+ result
46
+ end
47
+
48
+ def apply_filters(scope, base, filter)
49
+ filter.each do |key, val|
50
+ key = Sieve::Helpers.real_column_name(scope, key)
51
+ key_is_reflection = base.reflections.key? key
52
+ key_is_attribute = base.attribute_names.include? key
53
+ key_is_association_operator = ASSOCIATION_OPS.include? key
54
+ if key_is_association_operator
55
+ scope = apply_association_operator(base, scope, key, val)
56
+ elsif key_is_reflection || key_is_attribute
57
+ scope = apply_filter(scope, base, key, val)
58
+ end
59
+ end
60
+ scope
61
+ end
62
+
63
+ def apply_filter(scope, base, key, val)
64
+ reflection = base.reflections[key.to_s]
65
+ value_is_hash = val.is_a? Hash
66
+ if value_is_hash && reflection
67
+ # joined table filter, continue recursion, e.g. ?filter[blog][...]
68
+ association_base = reflection.klass
69
+ apply_filters(scope, association_base, val)
70
+ elsif reflection
71
+ # direct association id filter, e.g. ?filter[blog]=1
72
+ apply_association_filter(base, scope, key, val)
73
+ else
74
+ # column filter, e.g. ?filter[name]=...
75
+ apply_column_filter(base, scope, key, val)
76
+ end
77
+ end
78
+
79
+ def apply_association_filter(base, scope, key, value)
80
+ scope.where(base.arel_table[base.reflections[key].foreign_key].eq(value))
81
+ end
82
+
83
+ ASSOCIATION_OPS = %w[_is_empty].freeze
84
+ def apply_association_operator(base, scope, key, value)
85
+ if key == '_is_empty' && truthy?(value)
86
+ arel_node = base.arel_table['id']
87
+
88
+ where_clause = arel_node.eq(nil)
89
+ scope.where(where_clause)
90
+ else
91
+ scope
92
+ end
93
+ end
94
+
95
+ def apply_column_filter(base, scope, column_name, column_value)
96
+ if column_value.is_a?(Hash)
97
+ # it is a more complex query, possibly using operators like gt, lt, etc.
98
+ # more than one operator per-field is allowed
99
+ column_value.each do |operation, value|
100
+ scope = merge_where_clause(base, scope, column_name, value, operation)
101
+ end
102
+ else
103
+ scope = merge_where_clause(base, scope, column_name, column_value)
104
+ end
105
+ scope
106
+ end
107
+
108
+ BASIC_AREL_OPS = %w[eq gt lt gteq lteq].freeze
109
+ BASIC_AREL_COALESCE_OPS = BASIC_AREL_OPS.map { |op| "#{op}_coalesce" }.freeze
110
+ def merge_where_clause(base, scope, column_name, value, operation = nil) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
111
+ arel_node = base.arel_table[column_name]
112
+ where_clause = case operation
113
+ when *BASIC_AREL_OPS then arel_node.send(operation, value)
114
+ when *BASIC_AREL_COALESCE_OPS
115
+ coalesce = Arel::Nodes::NamedFunction.new('COALESCE', [arel_node, Arel::Nodes.build_quoted(value)])
116
+ coalesce.send(operation.split('_').first, value)
117
+ when 'diff' then arel_node.not_eq(value)
118
+ when 'is_null' then apply_is_null(arel_node, value)
119
+ when /^tree_(.*)/ then apply_tree(Regexp.last_match(1), base, column_name, arel_node, value)
120
+ else
121
+ if value.is_a? Array
122
+ arel_node.in(value)
123
+ else
124
+ arel_node.eq(value)
125
+ end
126
+ end
127
+ scope.where(where_clause)
128
+ end
129
+
130
+ def apply_is_null(arel_node, value)
131
+ if ActiveModel::Type::Boolean::FALSE_VALUES.include?(value)
132
+ arel_node.not_eq(nil)
133
+ else
134
+ arel_node.eq(nil)
135
+ end
136
+ end
137
+
138
+ def apply_tree(operation, base, column_name, arel_node, value)
139
+ subquery = base.where(column_name => value).map(&operation.to_sym).flatten.map(&column_name.to_sym)
140
+
141
+ arel_node.in(subquery)
142
+ end
143
+
144
+ def truthy?(value)
145
+ value.present? && ActiveModel::Type::Boolean::FALSE_VALUES.exclude?(value)
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rhino
4
+ module Sieve
5
+ class Geospatial
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ # geospatial={near: {location: { latitude: 41.2565, longitude: -95.9345 }, max_distance: 10, units: 'km'}}
11
+ # geospatial={near: {location: 'Omaha, NE, US', max_distance: 10, units: 'km'}}
12
+ def resolve(scope, params)
13
+ return @app.resolve(scope, params) unless valid?(params)
14
+
15
+ @app.resolve(
16
+ scope.near(
17
+ @location_params,
18
+ @near_params[:max_distance],
19
+ units: @near_params[:units]
20
+ ), params
21
+ )
22
+ end
23
+
24
+ private
25
+ def valid?(params) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
26
+ @geospatial_params = params[:geospatial]
27
+ return false unless @geospatial_params.is_a?(ActionController::Parameters)
28
+
29
+ @near_params = @geospatial_params[:near]
30
+ return false unless @near_params.is_a?(ActionController::Parameters)
31
+
32
+ @location_params = @near_params[:location]
33
+ if @location_params.is_a?(String)
34
+ @location_params = @near_params[:location]
35
+ elsif @location_params.is_a?(ActionController::Parameters) && @location_params[:latitude].present? && @location_params[:longitude].present?
36
+ @location_params = [@location_params[:latitude], @location_params[:longitude]]
37
+ else
38
+ return false
39
+ end
40
+
41
+ @near_params[:max_distance].present?
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rhino
4
+ module Sieve
5
+ class Helpers
6
+ def self.real_column_name(scope, column_name)
7
+ scope.attribute_aliases[column_name] || column_name
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rhino
4
+ module Sieve
5
+ class Limit
6
+ attr_accessor :default_limit
7
+
8
+ def initialize(app, default_limit: 20)
9
+ @app = app
10
+
11
+ @default_limit = default_limit
12
+ end
13
+
14
+ # limit=20
15
+ def resolve(scope, params)
16
+ @app.resolve(scope.limit(params[:limit] || default_limit), params)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rhino
4
+ module Sieve
5
+ class Offset
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ # offset=20
11
+ def resolve(scope, params)
12
+ @app.resolve(scope.offset(params[:offset]), params)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rhino
4
+ module Sieve
5
+ class Order
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ # order=name
11
+ def resolve(scope, params)
12
+ @scope = scope
13
+ @param = params[:order]
14
+
15
+ # Always append id to the end of the order clause to ensure a stable sort for pagination
16
+ result = apply_order.order(:id)
17
+ @app.resolve(result, params)
18
+ end
19
+
20
+ private
21
+ def parse(param)
22
+ direction = parse_direction(param)
23
+ column_name = parse_column_name(param, direction)
24
+ [direction, column_name]
25
+ end
26
+
27
+ def parse_direction(param)
28
+ if param[0] == '-'
29
+ :desc
30
+ else
31
+ :asc
32
+ end
33
+ end
34
+
35
+ def parse_column_name(param, direction)
36
+ param = if direction == :asc
37
+ param
38
+ else
39
+ param[1..]
40
+ end
41
+ Sieve::Helpers.real_column_name(@scope, param)
42
+ end
43
+
44
+ def string?(param)
45
+ param.is_a? String
46
+ end
47
+
48
+ def get_joins(base, path, _last_base)
49
+ return {} if path.empty?
50
+
51
+ # Convert to nested hash
52
+ join_tables = path.reverse.inject({}) { |assigned_value, key| { key => assigned_value } }
53
+
54
+ # If InnerJoin was used instead of OuterJoin, a simple ORDER clause targeting an association's column
55
+ # would incur in a join being made, which in turn would exclude records with no association. This would
56
+ # be conceptually wrong, as an ORDER clause would be excluding records from the final result. In order to
57
+ # restore the previous behavior and exclude these records without parent, one could simply add a WHERE
58
+ # clause with something like `WHERE association.id IS NOT NULL`.
59
+ ArelHelpers.join_association(base, join_tables, Arel::Nodes::OuterJoin, {})
60
+ rescue StandardError
61
+ nil
62
+ end
63
+
64
+ def build_select_clause(model, path, field)
65
+ return model.arel_table[Arel.star] if path.empty?
66
+
67
+ model.arel_table[field]
68
+ end
69
+
70
+ def split_field_path(column_name)
71
+ chunks = column_name.split('.')
72
+ field = chunks.last
73
+ path = chunks.slice(0, chunks.length - 1)
74
+ [path, field]
75
+ end
76
+
77
+ def analyze(column_name)
78
+ path, field = split_field_path(column_name)
79
+
80
+ last_base = if path.last.blank?
81
+ @scope
82
+ else
83
+ path.last.classify.safe_constantize
84
+ end
85
+ return nil unless last_base&.attribute_names&.include? field
86
+
87
+ join_clauses = get_joins(@scope.klass, path, last_base)
88
+ select_clause = build_select_clause(last_base, path, field)
89
+
90
+ [join_clauses, last_base.arel_table[field], select_clause]
91
+ end
92
+
93
+ NULL_ORDERING = {
94
+ asc: :nulls_last,
95
+ desc: :nulls_first
96
+ }.freeze
97
+ def order(direction, column_name)
98
+ # nulls_last should generally be the desired user experience
99
+ join_clauses, order_clause, select_clause = analyze(column_name)
100
+ return nil unless join_clauses && order_clause
101
+
102
+ final_clause = order_clause
103
+ .send(direction)
104
+ .send(NULL_ORDERING[direction])
105
+ [join_clauses, final_clause, select_clause]
106
+ end
107
+
108
+ def build_clause(param)
109
+ return nil unless string?(param)
110
+
111
+ direction, column_name = parse(param)
112
+ order(direction, column_name)
113
+ end
114
+
115
+ def group_clauses(clauses)
116
+ join_clauses = clauses.map { |joins, _node, _select| joins }
117
+ order_clauses = clauses.map { |_joins, node, _select| node }
118
+ select_clauses = clauses.map { |_joins, _node, select| select }
119
+ [order_clauses, join_clauses, select_clauses]
120
+ end
121
+
122
+ def build_clauses
123
+ clauses = @param.split(',')
124
+ .map { |el| build_clause(el) }
125
+ .filter { |joins, node, select| joins && node && select }
126
+ group_clauses(clauses)
127
+ end
128
+
129
+ def apply_order
130
+ return @scope unless string?(@param)
131
+
132
+ order_clauses, join_clauses, select_clauses = build_clauses
133
+ if order_clauses.empty?
134
+ @scope
135
+ else
136
+ select_clauses << "#{@scope.klass.table_name}.*"
137
+ @scope = @scope.reorder(nil).select(select_clauses.uniq).joins(join_clauses)
138
+ order_clauses.inject(@scope) { |acc, el| acc.order(el) }
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rhino
4
+ module Sieve
5
+ class Search
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ # search='test'
11
+ def resolve(scope, params)
12
+ return @app.resolve(scope, params) unless scope.respond_to?(:search_text_fields) && params[:search].present?
13
+
14
+ # Re-order so that rank isn't the default
15
+ # FIXME: Should look at the order param for 'rank'
16
+ @app.resolve(scope.search_text_fields(params[:search]).reorder(''), params)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rhino
4
+ module Sieve
5
+ extend ActiveSupport::Autoload
6
+
7
+ autoload :Filter
8
+ autoload :Geospatial
9
+ autoload :Limit
10
+ autoload :Offset
11
+ autoload :Order
12
+ autoload :Search
13
+ autoload :Helpers
14
+ end
15
+
16
+ class SieveStack
17
+ class Sieve
18
+ attr_reader :args, :block, :klass
19
+
20
+ def initialize(klass, args, block)
21
+ @klass = klass
22
+ @args = args
23
+ @block = block
24
+ end
25
+
26
+ delegate :name, to: :klass
27
+
28
+ def ==(other)
29
+ case other
30
+ when Sieve
31
+ klass == other.klass
32
+ when Class
33
+ klass == other
34
+ end
35
+ end
36
+
37
+ def inspect
38
+ if klass.is_a?(Class)
39
+ klass.to_s
40
+ else
41
+ klass.class.to_s
42
+ end
43
+ end
44
+
45
+ def build(app)
46
+ klass.new(app, *args, &block)
47
+ end
48
+
49
+ def build_instrumented(app)
50
+ InstrumentationProxy.new(build(app), inspect)
51
+ end
52
+ end
53
+
54
+ # This class is used to instrument the execution of a single sieve.
55
+ # It proxies the `s` method transparently and instruments the method
56
+ # call.
57
+ class InstrumentationProxy
58
+ EVENT_NAME = 'rhino.apply_sieve'
59
+
60
+ def initialize(sieve, class_name)
61
+ @sieve = sieve
62
+
63
+ @payload = {
64
+ sieve: class_name
65
+ }
66
+ end
67
+
68
+ def resolve(scope, params)
69
+ ActiveSupport::Notifications.instrument(EVENT_NAME, @payload) do
70
+ @sieve.resolve(scope, params)
71
+ end
72
+ end
73
+ end
74
+
75
+ # Pretty much stolen from ActionDispatch::MiddlewareStack
76
+ include Enumerable
77
+
78
+ attr_accessor :sieves
79
+
80
+ def initialize(*_args)
81
+ @sieves = []
82
+ yield(self) if block_given?
83
+ end
84
+
85
+ # rubocop:todo Style/ExplicitBlockArgument
86
+ def each
87
+ @sieves.each { |x| yield x }
88
+ end
89
+ # rubocop:enable Style/ExplicitBlockArgument
90
+
91
+ delegate :[], :size, :last, to: :sieves
92
+
93
+ def unshift(klass, *args, &block)
94
+ sieves.unshift(build_sieve(klass, args, block))
95
+ end
96
+ ruby2_keywords(:unshift) if respond_to?(:ruby2_keywords, true)
97
+
98
+ def initialize_copy(other)
99
+ self.sieves = other.sieves.dup
100
+ end
101
+
102
+ def insert(index, klass, *args, &block)
103
+ index = assert_index(index, :before)
104
+ sieves.insert(index, build_sieve(klass, args, block))
105
+ end
106
+ ruby2_keywords(:insert) if respond_to?(:ruby2_keywords, true)
107
+
108
+ alias insert_before insert
109
+
110
+ def insert_after(index, *args, &block)
111
+ index = assert_index(index, :after)
112
+ insert(index + 1, *args, &block)
113
+ end
114
+ ruby2_keywords(:insert_after) if respond_to?(:ruby2_keywords, true)
115
+
116
+ def swap(target, *args, &block)
117
+ index = assert_index(target, :before)
118
+ insert(index, *args, &block)
119
+ sieves.delete_at(index + 1)
120
+ end
121
+ ruby2_keywords(:swap) if respond_to?(:ruby2_keywords, true)
122
+
123
+ def delete(target)
124
+ sieves.delete_if { |m| m.klass == target }
125
+ end
126
+
127
+ def use(klass, *args, &block)
128
+ sieves.push(build_sieve(klass, args, block))
129
+ end
130
+ ruby2_keywords(:use) if respond_to?(:ruby2_keywords, true)
131
+
132
+ def build(app = nil, &block)
133
+ instrumenting = ActiveSupport::Notifications.notifier.listening?(InstrumentationProxy::EVENT_NAME)
134
+
135
+ # Freeze only production so that reloading works
136
+ sieves.freeze unless Rails.env.development?
137
+
138
+ sieves.reverse.inject(app || block) do |a, e|
139
+ if instrumenting
140
+ e.build_instrumented(a)
141
+ else
142
+ e.build(a)
143
+ end
144
+ end
145
+ end
146
+
147
+ private
148
+ def assert_index(index, where)
149
+ i = index.is_a?(Integer) ? index : sieves.index { |m| m.klass == index }
150
+ raise "No such sieve to insert #{where}: #{index.inspect}" unless i
151
+
152
+ i
153
+ end
154
+
155
+ def build_sieve(klass, args, block)
156
+ Sieve.new(klass, args, block)
157
+ end
158
+ end
159
+ end