rhino_project_core 0.20.0.beta.18

Sign up to get free protection for your applications and to get access to all the features.
Files changed (117) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +28 -0
  3. data/Rakefile +34 -0
  4. data/app/assets/stripe_flow.png +0 -0
  5. data/app/controllers/concerns/rhino/authenticated.rb +18 -0
  6. data/app/controllers/concerns/rhino/error_handling.rb +58 -0
  7. data/app/controllers/concerns/rhino/paper_trail_whodunnit.rb +11 -0
  8. data/app/controllers/concerns/rhino/permit.rb +38 -0
  9. data/app/controllers/concerns/rhino/set_current_user.rb +13 -0
  10. data/app/controllers/rhino/account_controller.rb +34 -0
  11. data/app/controllers/rhino/active_model_extension_controller.rb +52 -0
  12. data/app/controllers/rhino/base_controller.rb +23 -0
  13. data/app/controllers/rhino/crud_controller.rb +57 -0
  14. data/app/controllers/rhino/simple_controller.rb +11 -0
  15. data/app/controllers/rhino/simple_stream_controller.rb +12 -0
  16. data/app/helpers/rhino/omniauth_helper.rb +67 -0
  17. data/app/helpers/rhino/policy_helper.rb +42 -0
  18. data/app/helpers/rhino/segment_helper.rb +62 -0
  19. data/app/models/rhino/account.rb +13 -0
  20. data/app/models/rhino/current.rb +7 -0
  21. data/app/models/rhino/user.rb +44 -0
  22. data/app/overrides/active_record/autosave_association_override.rb +18 -0
  23. data/app/overrides/active_record/delegated_type_override.rb +14 -0
  24. data/app/overrides/activestorage/direct_uploads_controller_override.rb +23 -0
  25. data/app/overrides/activestorage/redirect_controller_override.rb +21 -0
  26. data/app/overrides/activestorage/redirect_representation_controller_override.rb +21 -0
  27. data/app/overrides/devise_token_auth/confirmations_controller_override.rb +14 -0
  28. data/app/overrides/devise_token_auth/omniauth_callbacks_controller_override.rb +45 -0
  29. data/app/overrides/devise_token_auth/passwords_controller_override.rb +9 -0
  30. data/app/overrides/devise_token_auth/registrations_controller_override.rb +20 -0
  31. data/app/overrides/devise_token_auth/sessions_controller_override.rb +26 -0
  32. data/app/overrides/devise_token_auth/token_validations_controller_override.rb +18 -0
  33. data/app/policies/rhino/account_policy.rb +27 -0
  34. data/app/policies/rhino/active_storage_attachment_policy.rb +16 -0
  35. data/app/policies/rhino/admin_policy.rb +20 -0
  36. data/app/policies/rhino/base_policy.rb +72 -0
  37. data/app/policies/rhino/crud_policy.rb +109 -0
  38. data/app/policies/rhino/editor_policy.rb +12 -0
  39. data/app/policies/rhino/global_policy.rb +8 -0
  40. data/app/policies/rhino/resource_info_policy.rb +9 -0
  41. data/app/policies/rhino/user_policy.rb +20 -0
  42. data/app/policies/rhino/viewer_policy.rb +19 -0
  43. data/app/resources/rhino/info_graph.rb +41 -0
  44. data/app/resources/rhino/open_api_info.rb +108 -0
  45. data/config/routes.rb +19 -0
  46. data/db/migrate/20180101000000_devise_token_auth_create_users.rb +54 -0
  47. data/db/migrate/20180622142754_add_allow_change_password_to_users.rb +5 -0
  48. data/db/migrate/20191217010224_add_approved_to_users.rb +7 -0
  49. data/db/migrate/20200503182019_change_tokens_to_json_b.rb +9 -0
  50. data/lib/commands/rhino_command.rb +59 -0
  51. data/lib/generators/rhino/dev/setup/setup_generator.rb +175 -0
  52. data/lib/generators/rhino/dev/setup/templates/env.client.tt +4 -0
  53. data/lib/generators/rhino/dev/setup/templates/env.server.tt +35 -0
  54. data/lib/generators/rhino/dev/setup/templates/prepare-commit-msg +17 -0
  55. data/lib/generators/rhino/install/install_generator.rb +24 -0
  56. data/lib/generators/rhino/install/templates/account.rb +4 -0
  57. data/lib/generators/rhino/install/templates/rhino.rb +24 -0
  58. data/lib/generators/rhino/install/templates/user.rb +4 -0
  59. data/lib/generators/rhino/module/module_generator.rb +93 -0
  60. data/lib/generators/rhino/module/templates/engine.rb.tt +19 -0
  61. data/lib/generators/rhino/module/templates/install_generator.rb.tt +12 -0
  62. data/lib/generators/rhino/module/templates/module_tasks.rake.tt +17 -0
  63. data/lib/generators/rhino/policy/policy_generator.rb +33 -0
  64. data/lib/generators/rhino/policy/templates/policy.rb.tt +46 -0
  65. data/lib/generators/test_unit/rhino_policy_generator.rb +13 -0
  66. data/lib/generators/test_unit/templates/policy_test.rb.tt +57 -0
  67. data/lib/rhino/engine.rb +140 -0
  68. data/lib/rhino/omniauth/strategies/azure_o_auth2.rb +16 -0
  69. data/lib/rhino/resource/active_model_extension/backing_store/google_sheet.rb +89 -0
  70. data/lib/rhino/resource/active_model_extension/backing_store.rb +33 -0
  71. data/lib/rhino/resource/active_model_extension/describe.rb +38 -0
  72. data/lib/rhino/resource/active_model_extension/params.rb +70 -0
  73. data/lib/rhino/resource/active_model_extension/properties.rb +229 -0
  74. data/lib/rhino/resource/active_model_extension/reference.rb +50 -0
  75. data/lib/rhino/resource/active_model_extension/routing.rb +15 -0
  76. data/lib/rhino/resource/active_model_extension/serialization.rb +16 -0
  77. data/lib/rhino/resource/active_model_extension.rb +38 -0
  78. data/lib/rhino/resource/active_record_extension/describe.rb +44 -0
  79. data/lib/rhino/resource/active_record_extension/params.rb +213 -0
  80. data/lib/rhino/resource/active_record_extension/properties.rb +85 -0
  81. data/lib/rhino/resource/active_record_extension/properties_describe.rb +226 -0
  82. data/lib/rhino/resource/active_record_extension/reference.rb +50 -0
  83. data/lib/rhino/resource/active_record_extension/routing.rb +21 -0
  84. data/lib/rhino/resource/active_record_extension/search.rb +23 -0
  85. data/lib/rhino/resource/active_record_extension/serialization.rb +16 -0
  86. data/lib/rhino/resource/active_record_extension.rb +30 -0
  87. data/lib/rhino/resource/active_record_tree.rb +50 -0
  88. data/lib/rhino/resource/active_storage_extension.rb +41 -0
  89. data/lib/rhino/resource/describe.rb +19 -0
  90. data/lib/rhino/resource/owner.rb +172 -0
  91. data/lib/rhino/resource/params.rb +31 -0
  92. data/lib/rhino/resource/properties.rb +192 -0
  93. data/lib/rhino/resource/reference.rb +31 -0
  94. data/lib/rhino/resource/routing.rb +107 -0
  95. data/lib/rhino/resource/serialization.rb +13 -0
  96. data/lib/rhino/resource/sieves.rb +36 -0
  97. data/lib/rhino/resource.rb +54 -0
  98. data/lib/rhino/sieve/filter.rb +149 -0
  99. data/lib/rhino/sieve/helpers.rb +11 -0
  100. data/lib/rhino/sieve/limit.rb +20 -0
  101. data/lib/rhino/sieve/offset.rb +16 -0
  102. data/lib/rhino/sieve/order.rb +143 -0
  103. data/lib/rhino/sieve/search.rb +20 -0
  104. data/lib/rhino/sieve.rb +158 -0
  105. data/lib/rhino/test_case/controller.rb +134 -0
  106. data/lib/rhino/test_case/override.rb +19 -0
  107. data/lib/rhino/test_case/policy.rb +76 -0
  108. data/lib/rhino/test_case.rb +10 -0
  109. data/lib/rhino/version.rb +17 -0
  110. data/lib/rhino.rb +129 -0
  111. data/lib/tasks/rhino.rake +38 -0
  112. data/lib/tasks/rhino_dev.rake +17 -0
  113. data/lib/validators/country_validator.rb +11 -0
  114. data/lib/validators/email_validator.rb +8 -0
  115. data/lib/validators/ipv4_validator.rb +10 -0
  116. data/lib/validators/mac_address_validator.rb +9 -0
  117. metadata +178 -0
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rhino
4
+ module Resource
5
+ module ActiveModelExtension
6
+ module Reference
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ scope :eager_load_refs, -> { includes(klass.references) } if defined?(scope)
11
+
12
+ def references_for_serialization
13
+ serialize_references(references)
14
+ end
15
+
16
+ private
17
+ # FIXME: Duplicated in params.rb
18
+ def reference_to_sym(reference)
19
+ reference.is_a?(Hash) ? reference.keys.first : reference
20
+ end
21
+
22
+ # FIXME: Duplicated in params.rb
23
+ def reference_from_sym(sym)
24
+ ref = try(sym)
25
+ return unless ref
26
+
27
+ # This is mostly how serializable_hash does it
28
+ # Get the first object
29
+ return ref.first if ref.respond_to?(:to_ary)
30
+
31
+ ref
32
+ end
33
+
34
+ def serialize_references(references)
35
+ hash = {}
36
+ references.each do |ref_item|
37
+ sym = reference_to_sym(ref_item)
38
+
39
+ hash[sym] = {}
40
+ hash[sym][:methods] = :display_name
41
+ hash[sym][:include] = serialize_references(ref_item[sym]) if ref_item.is_a?(Hash)
42
+ end.flatten.compact
43
+
44
+ hash
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rhino
4
+ module Resource
5
+ module ActiveModelExtension
6
+ module Routing
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ delegate :route_key, to: :model_name
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rhino
4
+ module Resource
5
+ module ActiveModelExtension
6
+ module Serialization
7
+ extend ActiveSupport::Concern
8
+
9
+ def to_caching_json
10
+ serializable_hash(methods: :display_name, include: references_for_serialization)
11
+ # JSON.generate(hash)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../resource"
4
+ require_relative "active_model_extension/properties"
5
+ require_relative "active_model_extension/reference"
6
+ require_relative "active_model_extension/describe"
7
+ require_relative "active_model_extension/routing"
8
+ require_relative "active_model_extension/params"
9
+ require_relative "active_model_extension/serialization"
10
+ require_relative "active_model_extension/backing_store"
11
+
12
+ module Rhino
13
+ module Resource
14
+ module ActiveModelExtension
15
+ extend ActiveSupport::Concern
16
+
17
+ # The minimum needed to make this work
18
+ include ActiveModel::Model
19
+ include ActiveModel::Attributes
20
+ include ActiveModel::Serializers::JSON
21
+
22
+ # Base
23
+ include Rhino::Resource
24
+
25
+ # Active Model implementations
26
+ include Rhino::Resource::ActiveModelExtension::Properties
27
+ include Rhino::Resource::ActiveModelExtension::Reference
28
+ include Rhino::Resource::ActiveModelExtension::Describe
29
+ include Rhino::Resource::ActiveModelExtension::Routing
30
+ include Rhino::Resource::ActiveModelExtension::Params
31
+ include Rhino::Resource::ActiveModelExtension::Serialization
32
+
33
+ included do
34
+ rhino_controller :active_model_extension
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rhino
4
+ module Resource
5
+ module ActiveRecordExtension
6
+ module Describe
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ def describe # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
11
+ properties = all_properties.index_with { |p| describe_property(p) }
12
+
13
+ required = properties.reject { |_p, d| d[:nullable] || d[:readOnly] }.keys
14
+ # required: [] is not valid and will be compacted away
15
+ required = nil unless required.present?
16
+
17
+ {
18
+ "x-rhino-model": {
19
+ model: model_name.singular,
20
+ modelPlural: model_name.collection,
21
+ name: model_name.name.camelize(:lower),
22
+ pluralName: model_name.name.camelize(:lower).pluralize,
23
+ readableName: model_name.human,
24
+ pluralReadableName: model_name.human.pluralize,
25
+ ownedBy: resource_owned_by,
26
+ singular: route_singular?,
27
+ path: route_api,
28
+ searchable: searchable?
29
+ },
30
+ type: :object,
31
+ properties:,
32
+ required:
33
+ }.compact
34
+ end
35
+
36
+ # returns true if the model's rhino_search is set with at least one field
37
+ def searchable?
38
+ @rhino_is_searchable == true
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rhino
4
+ module Resource
5
+ module ActiveRecordExtension
6
+ module Params # rubocop:todo Metrics/ModuleLength
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do # rubocop:todo Metrics/BlockLength
10
+ def create_params
11
+ writeable_params("create")
12
+ end
13
+
14
+ def show_params
15
+ readable_params("show")
16
+ end
17
+
18
+ def update_params
19
+ writeable_params("update")
20
+ end
21
+
22
+ def transform_params(params)
23
+ transform_params_recursive(params)
24
+ end
25
+
26
+ protected
27
+ def reference_to_sym(reference)
28
+ reference.is_a?(Hash) ? reference.keys.first : reference
29
+ end
30
+
31
+ def assoc_from_sym(sym, base = self)
32
+ assoc = base.reflect_on_association(sym)
33
+
34
+ # Delegate types support bolstered by rhino/rhino/app/overrides/active_record/delegated_type_override.rb
35
+ return assoc.active_record.send("#{assoc.name}_types").map(&:constantize) if assoc.options[:polymorphic]
36
+
37
+ [assoc.klass]
38
+ end
39
+
40
+ def props_by_type(type)
41
+ # FIXME: Direct attributes for this model we want a copy, not to
42
+ # alter the class_attribute itself
43
+ send("#{type}_properties").dup
44
+ end
45
+
46
+ def readable_params(type, refs = references) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
47
+ params = []
48
+
49
+ refs_index = refs.index_by { |r| reference_to_sym(r) }
50
+
51
+ props_by_type("read").each do |prop|
52
+ desc = describe_property(prop)
53
+ prop_sym = prop.to_sym
54
+
55
+ # If its a reference or an array of references
56
+ # FIXME: anyOf is a hack for now
57
+ if desc[:type] == :reference || (desc[:type] == :array && (desc[:items].key?(:$ref) || desc[:items].key?(:anyOf)))
58
+ next unless refs_index.key?(prop_sym)
59
+
60
+ next_refs = refs_index[prop_sym].is_a?(Hash) ? refs_index[prop_sym][prop_sym] : []
61
+ klasses = assoc_from_sym(prop_sym)
62
+
63
+ assoc_params = klasses.map { |klass| klass.send(:readable_params, type, next_refs) }
64
+
65
+ next params << { prop.to_s => assoc_params.flatten.uniq }
66
+ end
67
+
68
+ # JSON columns need special handling - allow all the nested params
69
+ next params << { prop => {} } if desc[:type].in?(%i[json jsonb])
70
+
71
+ # Generic array of scalars
72
+ next params << { prop => [] } if desc[:type] == :array
73
+
74
+ # Otherwise prop and param are equivalent
75
+ params << prop
76
+ end
77
+
78
+ # Display name is always allowed
79
+ params << "display_name"
80
+ end
81
+
82
+ def writeable_params(type, _refs = references) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
83
+ params = []
84
+
85
+ props_by_type(type).each do |prop| # rubocop:disable Metrics/BlockLength
86
+ desc = describe_property(prop)
87
+ prop_sym = prop.to_sym
88
+
89
+ # An array of references
90
+ if desc[:type] == :array && (desc[:items].key?(:$ref) || desc[:items].key?(:anyOf))
91
+ # FIXME: Hack for has_many_attached
92
+ next params << { prop => [] } if desc.dig(:items, :anyOf)[0]&.dig(:$ref) == "#/components/schemas/active_storage_attachment"
93
+
94
+ # We only accept if the active record accepts it
95
+ next unless nested_attributes_options.key?(prop_sym) || desc.dig(:items, :anyOf)[0]&.dig(:$ref)
96
+
97
+ assoc = assoc_from_sym(prop_sym).first
98
+
99
+ # This does not handle nested for a polymorphic model, but neither does rails
100
+ # FIXME: Do we need to handle :update_only option?
101
+ # FIXME: If a nested resource attribute is create only, the backend will allow it to be
102
+ # updated because create/update are merged together - the frontedn UI shows the right
103
+ # thing though NUB-844
104
+ assoc_params = []
105
+
106
+ array_attributes = desc[:items][:"x-rhino-attribute-array"]
107
+
108
+ if array_attributes[:updatable]
109
+ # If the attribute is updatable, we need to accept the id param
110
+ assoc_params << assoc.identifier_property
111
+
112
+ # If the attribute is updatable, we need to accept its updatable params
113
+ assoc_params << assoc.send(:writeable_params, "update")
114
+ end
115
+
116
+ # If the attribute is creatable, we need to accept its creatable params
117
+ assoc_params << assoc.send(:writeable_params, "create") if array_attributes[:creatable]
118
+
119
+ # If its destroyable, accept the _destroy param
120
+ assoc_params << "_destroy" if nested_attributes_options[prop_sym][:allow_destroy]
121
+
122
+ next params << { prop => assoc_params.flatten.uniq }
123
+ end
124
+
125
+ # JSON columns need special handling - allow all the nested params
126
+ next params << { prop => {} } if desc[:type].in?(%i[json jsonb])
127
+
128
+ # Generic array of scalars
129
+ next params << { prop => [] } if desc[:type] == :array
130
+
131
+ # Accept { blog_post: { :id }} as well as { blog_post: 3 } below
132
+ if desc[:type] == :reference
133
+ klasses = assoc_from_sym(prop_sym)
134
+
135
+ params << if nested_attributes_options.key?(prop_sym)
136
+ assoc_params = klasses.map(&:identifier_property)
137
+ assoc_params << klasses.map { |klass| klass.send(:writeable_params, "update") }
138
+
139
+ { prop => assoc_params.flatten.uniq }
140
+ else
141
+ { prop => klasses.map(&:identifier_property).uniq }
142
+ end
143
+ end
144
+
145
+ # Otherwise prop and param are equivalent
146
+ # We also accept the ref name as the foreign key if its a singular resource
147
+ params << prop
148
+ end
149
+
150
+ params
151
+ end
152
+
153
+ # Rebuild the params
154
+ # rubocop:todo Metrics/CyclomaticComplexity
155
+ def transform_params_recursive(params, parent = self) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
156
+ hash = {}
157
+ params.each do |param_key, param_value|
158
+ association = parent.reflect_on_association(param_key)
159
+
160
+ # Its a regular attribute
161
+ next hash[param_key] = param_value unless association
162
+
163
+ # FIXME
164
+ # Hack to rewrite for attachment/attachments and guard against object resubmission
165
+ if param_key.end_with?("_attachment")
166
+ hash[param_key.remove("_attachment")] = param_value if param_value.is_a?(String) || param_value.nil?
167
+
168
+ next
169
+ end
170
+ if param_key.end_with?("_attachments")
171
+ hash[param_key.remove("_attachments")] = param_value if param_value.is_a?(Array) || param_value.nil?
172
+
173
+ next
174
+ end
175
+
176
+ # Transform the nested attributes as well
177
+ # Nested need _attributes - we don't want the client to have to do that
178
+ if parent.nested_attributes_options.key?(param_key.to_sym)
179
+ attr_key = "#{param_key}_attributes"
180
+
181
+ # has_many nested should be an array
182
+ if association.macro == :has_many
183
+ next hash[attr_key] = param_value.map { |pv| parent.transform_params_recursive(pv, association.klass) }
184
+ end
185
+
186
+ # has_one/belongs_to is just the values
187
+ # if its a cardinal though, such as blog: 1 instead of blog: {name : 'my blog' }
188
+ # fallback to transforming to the foreign key
189
+ if param_value.is_a?(ActionController::Parameters)
190
+ klasses = assoc_from_sym(param_key, parent)
191
+
192
+ next hash[attr_key] = klasses.map { |klass| parent.transform_params_recursive(param_value, klass) }.reduce(:merge)
193
+ end
194
+ end
195
+
196
+ # Map association name to foreign key, ie blog => blog_id
197
+ # or blog: { id: } => blog_id
198
+ if param_value.is_a?(ActionController::Parameters)
199
+ next hash[association.foreign_key] = param_value[association.klass.identifier_property]
200
+ end
201
+
202
+ hash[association.foreign_key] = param_value
203
+ end
204
+
205
+ # Force permit since we should have already been permitted at this point
206
+ ActionController::Parameters.new(hash).permit!
207
+ end
208
+ # rubocop:enable Metrics/CyclomaticComplexity
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "properties_describe"
4
+
5
+ module Rhino
6
+ module Resource
7
+ module ActiveRecordExtension
8
+ module Properties
9
+ extend ActiveSupport::Concern
10
+
11
+ include Rhino::Resource::ActiveRecordExtension::PropertiesDescribe
12
+
13
+ class_methods do # rubocop:disable Metrics/BlockLength
14
+ def identifier_property
15
+ primary_key
16
+ end
17
+
18
+ def readable_properties
19
+ props = attribute_names - foreign_key_properties
20
+
21
+ props.concat(reference_properties)
22
+
23
+ props.map(&:to_s)
24
+ end
25
+
26
+ def creatable_properties
27
+ writeable_properties
28
+ end
29
+
30
+ def updatable_properties
31
+ # https://api.rubyonrails.org/classes/ActiveRecord/ReadonlyAttributes/ClassMethods.html#method-i-readonly_attributes
32
+ writeable_properties - readonly_attributes.to_a
33
+ end
34
+
35
+ private
36
+ # FIXME: Duplicated in params.rb
37
+ def reference_to_sym(reference)
38
+ reference.is_a?(Hash) ? reference.keys.first : reference
39
+ end
40
+
41
+ # FIXME: Include counter caches as well
42
+ def automatic_properties
43
+ [identifier_property] + send(:all_timestamp_attributes_in_model)
44
+ end
45
+
46
+ def virtual_properties
47
+ columns.select(&:virtual?).map(&:name)
48
+ end
49
+
50
+ def foreign_key_properties
51
+ reflect_on_all_associations(:belongs_to).map(&:foreign_key).map(&:to_s)
52
+ end
53
+
54
+ # rubocop:todo Style/OptionalBooleanParameter
55
+ def reference_properties(read = true)
56
+ references.filter_map do |r|
57
+ sym = reference_to_sym(r)
58
+
59
+ # All references are readable
60
+ next sym if read
61
+
62
+ # Writeable if a one type or accepting nested
63
+ association = reflect_on_association(sym)
64
+ # rubocop:todo Performance/CollectionLiteralInLoop, Layout/LineLength
65
+ if %i[has_one belongs_to].include?(association.macro) || nested_attributes_options.key?(sym) || association.class_name == "ActiveStorage::Attachment"
66
+ sym
67
+ end
68
+ # rubocop:enable Performance/CollectionLiteralInLoop, Layout/LineLength
69
+ end
70
+ end
71
+ # rubocop:enable Style/OptionalBooleanParameter
72
+
73
+ def writeable_properties
74
+ # Direct properties for this model
75
+ props = attribute_names - automatic_properties - virtual_properties - foreign_key_properties
76
+
77
+ props.concat(reference_properties(false))
78
+
79
+ props.map(&:to_s)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rhino
4
+ module Resource
5
+ module ActiveRecordExtension
6
+ module PropertiesDescribe # rubocop:disable Metrics/ModuleLength
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do # rubocop:disable Metrics/BlockLength
10
+ def describe_property(property) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
11
+ name = property_name(property).to_s
12
+ raise StandardError, "#{name} is not a valid property" unless property?(name)
13
+
14
+ {
15
+ "x-rhino-attribute": {
16
+ name:,
17
+ readableName: name.titleize,
18
+ readable: read_properties.include?(property),
19
+ creatable: create_properties.include?(property),
20
+ updatable: update_properties.include?(property)
21
+ },
22
+ readOnly: property_read_only?(name),
23
+ writeOnly: property_write_only?(name),
24
+ nullable: property_nullable?(name),
25
+ default: property_default(name)
26
+ }
27
+ .merge(property_type_and_format(property))
28
+ .merge(property_validations(property))
29
+ .deep_merge(property_overrides(property))
30
+ .compact
31
+ end
32
+
33
+ private
34
+ # FIXME: It can be a hash if passed in from a reference which might have something like
35
+ # rhino_references %i[{blog_posts: [:comments]}]
36
+ # but I cannot find current spot where it is used like that currently
37
+ def property_name(property)
38
+ property.is_a?(Hash) ? property.keys.first : property
39
+ end
40
+
41
+ def ref_descriptor(names)
42
+ {
43
+ type: :reference,
44
+ anyOf: names.map { |name| { :$ref => "#/components/schemas/#{name.singularize}" } }
45
+ }
46
+ end
47
+
48
+ def property_type_and_format_attr(name) # rubocop:todo Metrics/MethodLength
49
+ atype = attribute_types[name.to_s].type
50
+
51
+ # The PG array delegates type to "subtype" which is the actual type of the array elements
52
+ if attribute_types[name.to_s].is_a? ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array
53
+ return {
54
+ type: :array,
55
+ items: {
56
+ type: atype
57
+ }
58
+ }
59
+ end
60
+
61
+ if %i[datetime date time].include?(atype)
62
+ return {
63
+ type: 'string',
64
+ format: atype
65
+ }
66
+ end
67
+
68
+ { type: atype }
69
+ end
70
+
71
+ def nested_array_options(name)
72
+ ref_sym = name.to_sym
73
+
74
+ array_options = {}
75
+
76
+ if nested_attributes_options[ref_sym]
77
+ array_options[:creatable] = true
78
+ array_options[:updatable] = true
79
+ array_options[:destroyable] = nested_attributes_options[ref_sym][:allow_destroy]
80
+ end
81
+
82
+ { "x-rhino-attribute-array": array_options.merge(_properties_array[ref_sym] || {}) }
83
+ end
84
+
85
+ # rubocop:todo Metrics/PerceivedComplexity
86
+ # rubocop:todo Metrics/AbcSize
87
+ def property_type_and_format_ref(name) # rubocop:todo Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/PerceivedComplexity
88
+ assoc = reflections[name]
89
+ klasses = if assoc.options[:polymorphic]
90
+ assoc.active_record.send("#{assoc.name}_types").map(&:constantize).map { |m| m.model_name.singular }
91
+ else
92
+ # FIXME: The tr hack is to match how model_name in rails handles modularized classes
93
+ [assoc.options[:class_name]&.underscore&.tr('/', '_') || name]
94
+ end
95
+
96
+ return ref_descriptor(klasses) unless reflections[name].macro == :has_many
97
+
98
+ {
99
+ type: :array,
100
+ items: ref_descriptor(klasses).merge(nested_array_options(name))
101
+ }
102
+ end
103
+ # rubocop:enable Metrics/AbcSize
104
+ # rubocop:enable Metrics/PerceivedComplexity
105
+
106
+ def property_type_and_format(name) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
107
+ # Special cases
108
+ return { type: :identifier } if name == identifier_property
109
+ return { type: :string } if defined_enums.key?(name)
110
+
111
+ # FIXME: Hack for tags for now
112
+ if attribute_types.key?(name.to_s) && attribute_types[name.to_s].class.to_s == 'ActsAsTaggableOn::Taggable::TagListType'
113
+ return {
114
+ type: :array,
115
+ items: {
116
+ type: 'string'
117
+ }
118
+ }
119
+ end
120
+
121
+ # Use the attribute type if possible
122
+ return property_type_and_format_attr(name) if attribute_types.key?(name.to_s)
123
+
124
+ return property_type_and_format_ref(name) if reflections.key?(name)
125
+
126
+ # FIXME: There may be no way to reach this
127
+ # raise UnknownpropertyType
128
+ { type: :unknown }
129
+ end
130
+
131
+ def property_overrides(property)
132
+ return {} unless _properties_overrides.key?(property)
133
+
134
+ _properties_overrides[property].deep_symbolize_keys
135
+ end
136
+
137
+ def property_validations(property) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
138
+ constraint_hash = {}
139
+
140
+ # https://swagger.io/specification/
141
+
142
+ validators_on(property).each do |v|
143
+ if v.is_a? ActiveModel::Validations::NumericalityValidator
144
+ if v.options.key?(:greater_than)
145
+ constraint_hash[:minimum] = v.options[:greater_than]
146
+ constraint_hash[:exclusiveMinimum] = true
147
+ end
148
+
149
+ if v.options.key?(:less_than)
150
+ constraint_hash[:maximum] = v.options[:less_than]
151
+ constraint_hash[:exclusiveMaximum] = true
152
+ end
153
+
154
+ constraint_hash[:minimum] = v.options[:greater_than_or_equal_to] if v.options.key?(:greater_than_or_equal_to)
155
+ constraint_hash[:maximum] = v.options[:less_than_or_equal_to] if v.options.key?(:less_than_or_equal_to)
156
+
157
+ if v.options.key?(:in)
158
+ constraint_hash[:minimum] = v.options[:in].min
159
+ constraint_hash[:maximum] = v.options[:in].max
160
+ end
161
+ end
162
+
163
+ if v.is_a? ::ActiveRecord::Validations::LengthValidator
164
+ constraint_hash[:minLength] = v.options[:minimum] || v.options[:is]
165
+ constraint_hash[:maxLength] = v.options[:maximum] || v.options[:is]
166
+ end
167
+
168
+ constraint_hash[:pattern] = JsRegex.new(v.options[:with]).source if v.is_a? ::ActiveModel::Validations::FormatValidator
169
+
170
+ constraint_hash[:enum] = v.options[:in] if v.is_a? ActiveModel::Validations::InclusionValidator
171
+ end
172
+
173
+ constraint_hash[:enum] = defined_enums[property].keys if defined_enums.key?(property)
174
+
175
+ constraint_hash.compact
176
+ end
177
+
178
+ # If there is a presence validator in the model it is not nullable.
179
+ # if there is no optional: true on an association, rails will add a
180
+ # presence validator automatically
181
+ # Otherwise check the db for the actual column or foreign key setting
182
+ # Return nil instead of false for compaction
183
+ def property_nullable?(name) # rubocop:todo Metrics/AbcSize
184
+ # Check for any presence validator
185
+ return false if validators_on(name).any?(::ActiveRecord::Validations::PresenceValidator)
186
+
187
+ # https://guides.rubyonrails.org/active_record_validations.html#numericality
188
+ # By default, numericality doesn't allow nil values. You can use allow_nil: true option to permit it.
189
+ validators_on(name).select { |v| v.is_a? ::ActiveRecord::Validations::NumericalityValidator }.each do |v|
190
+ return false unless v.options[:allow_nil]
191
+ end
192
+
193
+ name = reflections[name].foreign_key if reflections.key?(name)
194
+
195
+ # Check the column null setting
196
+ return columns_hash[name].null if columns_hash.key?(name)
197
+
198
+ true
199
+ end
200
+
201
+ # Return nil instead of false for compaction
202
+ def property_read_only?(name)
203
+ return unless read_properties.include?(name) && (create_properties.exclude?(name) && update_properties.exclude?(name))
204
+
205
+ true
206
+ end
207
+
208
+ # Return nil instead of false for compaction
209
+ def property_write_only?(name)
210
+ return unless (create_properties.include?(name) || update_properties.include?(name)) && read_properties.exclude?(name)
211
+
212
+ true
213
+ end
214
+
215
+ def property_default(name)
216
+ # FIXME: This will not handle datetime fields
217
+ # https://github.com/rails/rails/issues/27077 sets the default in the db
218
+ # but Blog.new does not set the default value like other attributes
219
+ # https://nubinary.atlassian.net/browse/NUB-298
220
+ _default_attributes[name].type_cast(_default_attributes[name].value_before_type_cast)
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end
226
+ end