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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestUnit
4
+ module Generators
5
+ class RhinoPolicyGenerator < ::Rails::Generators::NamedBase
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ def create_policy_test
9
+ template "policy_test.rb", File.join("test/policies", class_path, "#{file_name}_policy_test.rb")
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class <%= class_name %>PolicyTest < Rhino::TestCase::Policy
6
+ # Testing for a policy where users can view any blog, create, update and destroy their own
7
+ # but cannot update or destroy another users blog.
8
+
9
+ # def setup
10
+ # @current_user = create :user
11
+ # @another_user = create :user
12
+
13
+ # @blog = Blog.create(title: "Test Blog", author: @current_user)
14
+ # @another_user_blog = Blog.create(title: "Other Blog", author: @another_user)
15
+ # end
16
+
17
+ # test "allows create for user" do
18
+ # assert_permit @current_user, Blog, :create
19
+ # end
20
+
21
+ # test "allows update for user" do
22
+ # assert_permit @current_user, @blog, :update
23
+ # end
24
+
25
+ # test "does not allow update for another users blog" do
26
+ # assert_not_permit @current_user, @another_user_blog, :update
27
+ # end
28
+
29
+ # test "allows destroy for user" do
30
+ # assert_permit @current_user, @blog, :destroy
31
+ # end
32
+
33
+ # test "does not allow destroy for another users blog" do
34
+ # assert_not_permit @current_user, @another_user_blog, :destroy
35
+ # end
36
+
37
+ # test "allows index for user and returns correct blogs" do
38
+ # assert_permit @current_user, Blog, :index
39
+ # assert_scope_only @current_user, Blog, [@blog, @another_user_blog]
40
+ # end
41
+
42
+ # test "allows show for user and returns correct blog" do
43
+ # assert_permit @current_user, @blog, :show
44
+ # assert_scope_only @current_user, @blog, [@blog]
45
+ # end
46
+
47
+ # test "allows show for another users blog" do
48
+ # assert_permit @current_user, @another_user_blog, :show
49
+ # assert_scope_only @current_user, [@another_user_blog]
50
+ # end
51
+
52
+ # If the user could instead not show another users blog, the following test could be used instead
53
+ ## test "does not allow show for another users blog" do
54
+ ## assert_not_permit @current_user, @another_user_blog, :show
55
+ ## assert_scope_empty @current_user, @another_user_blog
56
+ ## end
57
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rhino
4
+ class Engine < ::Rails::Engine
5
+ config.before_configuration do
6
+ # When running the dummy apps through rails commands the .env file won't exist in the root
7
+ # but the DB_NAME variable will have been set by rhino_command; the rake task name will be
8
+ # set for rhino:dev:setup
9
+ if Rails.env.development? && (!File.exist?(Rails.root.join(".env")) && (!run_from_dummy? && !run_from_dev_setup? && !run_from_package?))
10
+ raise ".env file must exist in development - see README.md"
11
+ end
12
+ end
13
+
14
+ initializer 'rhino.active_record_extension' do
15
+ ActiveSupport.on_load(:active_record) do
16
+ require_relative 'resource/active_record_extension'
17
+ require_relative 'resource/active_record_tree'
18
+ require_relative 'resource/active_model_extension'
19
+
20
+ include Rhino::Resource::ActiveRecordExtension if Rhino.auto_include_active_record
21
+ end
22
+ end
23
+
24
+ initializer 'rhino.active_storage_extension' do
25
+ ActiveSupport.on_load(:active_storage_attachment) do
26
+ require_relative 'resource/active_storage_extension'
27
+
28
+ include Rhino::Resource::ActiveStorageExtension
29
+ end
30
+ end
31
+
32
+ # https://guides.rubyonrails.org/engines.html#overriding-models-and-controllers
33
+ # Use root instead of Rails.root to scope for this engine
34
+ initializer 'rhino.overrides' do
35
+ overrides = "#{root}/app/overrides"
36
+ Rails.autoloaders.main.ignore(overrides)
37
+
38
+ config.to_prepare do
39
+ Dir.glob("#{overrides}/**/*_override.rb").each do |override|
40
+ load override
41
+ end
42
+ end
43
+ end
44
+
45
+ initializer 'rhino.check_resources' do
46
+ config.after_initialize do
47
+ check_resources
48
+ end
49
+ end
50
+
51
+ initializer 'rhino.resource_reloader' do
52
+ config.after_initialize do
53
+ Rails.application.reloader.to_prepare do
54
+ Rhino.resource_classes = nil
55
+ end
56
+ end
57
+ end
58
+
59
+ initializer 'rhino.register_module' do
60
+ require 'rhino/omniauth/strategies/azure_o_auth2'
61
+
62
+ config.after_initialize do
63
+ Rhino.registered_modules[:rhino] = {
64
+ version: Rhino::VERSION,
65
+ authOwner: Rhino.auth_owner.model_name.singular,
66
+ baseOwner: Rhino.base_owner.model_name.singular,
67
+ oauth: Rhino::OmniauthHelper.strategies_metadata,
68
+ allow_signup: Rhino.allow_signup
69
+ }
70
+ end
71
+ end
72
+
73
+ def top_level_references(resource)
74
+ # Handle things like rhino_references [{ blog_post: [:blog] }]
75
+ # Just check the top level ones for now
76
+ resource.references.flat_map { |ref| ref.is_a?(Hash) ? ref.keys : ref }
77
+ end
78
+
79
+ def unowned?(resource)
80
+ # Special case
81
+ return true if resource.ancestors.include?(Rhino::Resource::ActiveStorageExtension)
82
+
83
+ # Owners are not themselves owned
84
+ resource.auth_owner? || resource.base_owner? || resource.global_owner?
85
+ end
86
+
87
+ def check_owner_reflections
88
+ raise "#{Rhino.base_owner} must have reflection for #{Rhino.auth_owner}" if Rhino.base_to_auth.nil?
89
+
90
+ raise "#{Rhino.auth_owner} must have reflection for #{Rhino.base_owner}" if Rhino.auth_to_base.nil?
91
+ end
92
+
93
+ def check_ownership(resource)
94
+ return if unowned?(resource)
95
+
96
+ raise "#{resource} does not have rhino ownership set" unless resource.resource_owned_by.present?
97
+ end
98
+
99
+ def check_references(resource)
100
+ # Some resource types don't have reflections
101
+ top_level_reflections = resource.try(:reflections)&.keys&.map(&:to_sym) || []
102
+
103
+ # All references should have a reflection
104
+ delta = top_level_references(resource) - top_level_reflections
105
+
106
+ raise "#{resource} has references #{delta} that do not exist as associations" if delta.present?
107
+ end
108
+
109
+ def check_owner_reference(resource)
110
+ return if unowned?(resource)
111
+
112
+ # If its in the list, we're good
113
+ return if top_level_references(resource).include?(resource.resource_owned_by)
114
+
115
+ raise "#{resource} does not have a reference to its owner #{resource.resource_owned_by}"
116
+ end
117
+
118
+ def check_resources
119
+ check_owner_reflections
120
+
121
+ Rhino.resource_classes.each do |resource|
122
+ check_ownership(resource)
123
+ check_references(resource)
124
+ check_owner_reference(resource) if resource.ancestors.include?(Rhino::Resource::ActiveRecordExtension)
125
+ end
126
+ end
127
+
128
+ def self.run_from_dummy?
129
+ ENV["DB_NAME"].present?
130
+ end
131
+
132
+ def self.run_from_dev_setup?
133
+ Rake.application.top_level_tasks == ["rhino:dev:setup"]
134
+ end
135
+
136
+ def self.run_from_package?
137
+ Rake.application.top_level_tasks == ["package"]
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ return unless defined?(::OmniAuth::Strategies::AzureActivedirectoryV2)
4
+
5
+ module Rhino
6
+ module Omniauth
7
+ module Strategies
8
+ class AzureOAuth2 < ::OmniAuth::Strategies::AzureActivedirectoryV2
9
+ option :name, "azure_oauth2"
10
+ option :callback_path, "/api/auth/omniauth/azure_oauth2/callback"
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ ::OmniAuth::Strategies::AzureOauth2 = Rhino::Omniauth::Strategies::AzureOAuth2
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rhino
4
+ module Resource
5
+ module ActiveModelExtension
6
+ module BackingStore
7
+ module GoogleSheet
8
+ extend ActiveSupport::Concern
9
+
10
+ def backing_store_update
11
+ attr_map = sheet.list.keys.index_by(&:downcase)
12
+
13
+ # ID won't be mapped
14
+ attrs = serializable_hash(except: :id).transform_keys { |k| attr_map[k] }
15
+
16
+ sheet.list[id - 1].update(attrs)
17
+
18
+ sheet.synchronize
19
+ end
20
+
21
+ def backing_store_destroy
22
+ sheet.delete_rows(id + 1, 1)
23
+
24
+ sheet.synchronize
25
+ end
26
+
27
+ included do
28
+ thread_mattr_accessor :google_client
29
+ thread_mattr_accessor :google_sheet
30
+ thread_mattr_accessor :google_worksheet
31
+
32
+ class_attribute :sheet_id, default: nil
33
+ class_attribute :work_sheet_title, default: nil
34
+
35
+ delegate :sheet, to: :class
36
+ end
37
+
38
+ class_methods do # rubocop:todo Metrics/BlockLength
39
+ def backing_store_index
40
+ sheet.reload
41
+
42
+ idx = 0
43
+ sheet.list.map do |row|
44
+ idx += 1
45
+ row_to_instance(row, idx)
46
+ end
47
+ end
48
+
49
+ def backing_store_create(model)
50
+ attr_map = sheet.list.keys.index_by(&:downcase)
51
+
52
+ # ID won't be mapped
53
+ attrs = model.serializable_hash(except: :id).transform_keys { |k| attr_map[k] }
54
+
55
+ sheet.list.push(attrs)
56
+
57
+ sheet.synchronize
58
+ end
59
+
60
+ def backing_store_show(id)
61
+ sheet.reload
62
+
63
+ row_to_instance(sheet.list[id.to_i - 1], id)
64
+ end
65
+
66
+ def sheet
67
+ return @google_worksheet if @google_worksheet
68
+
69
+ @google_client = GoogleDrive::Session.from_service_account_key(nil)
70
+
71
+ # Pass the sheet id
72
+ @google_sheet = @google_client.spreadsheet_by_key(sheet_id)
73
+
74
+ return @google_worksheet = @google_sheet.worksheet_by_title(work_sheet_title) if work_sheet_title
75
+
76
+ @google_worksheet = @google_sheet.worksheets[0]
77
+ end
78
+
79
+ def row_to_instance(row, id)
80
+ attrs = row.to_hash
81
+ attrs = attrs.transform_keys(&:downcase).transform_keys(&:to_sym)
82
+ new(attrs.merge(id:))
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rhino
4
+ module Resource
5
+ module ActiveModelExtension
6
+ module BackingStore
7
+ extend ActiveSupport::Concern
8
+
9
+ def backing_store_update
10
+ raise NotImplementedError, "#update is not implemented for BackingStore"
11
+ end
12
+
13
+ def backing_store_destroy
14
+ raise NotImplementedError, "#destroy is not implemented for BackingStore"
15
+ end
16
+
17
+ class_methods do
18
+ def backing_store_create
19
+ raise NotImplementedError, "#create is not implemented for BackingStore"
20
+ end
21
+
22
+ def backing_store_index
23
+ raise NotImplementedError, "#index is not implemented for BackingStore"
24
+ end
25
+
26
+ def backing_store_show
27
+ raise NotImplementedError, "#show is not implemented for BackingStore"
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rhino
4
+ module Resource
5
+ module ActiveModelExtension
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: "#{Rhino.namespace}/#{route_path}"
28
+ },
29
+ type: :object,
30
+ properties:,
31
+ required:
32
+ }.compact
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rhino
4
+ module Resource
5
+ module ActiveModelExtension
6
+ module Params
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
+ protected
23
+ def props_by_type(type)
24
+ # FIXME: Direct attributes for this model we want a copy, not to
25
+ # alter the class_attribute itself
26
+ send("#{type}_properties").dup
27
+ end
28
+
29
+ # FIXME: Refs are not handled
30
+ def readable_params(_type, _refs = references)
31
+ params = []
32
+ props_by_type("read").each do |prop|
33
+ desc = describe_property(prop)
34
+
35
+ # Generic array of scalars
36
+ next params << { prop => [] } if desc[:type] == :array
37
+
38
+ # Otherwise prop and param are equivalent
39
+ params << prop
40
+ end
41
+
42
+ # Display name is always allowed
43
+ params << "display_name"
44
+ end
45
+
46
+ # FIXME: Refs are not handled
47
+ def writeable_params(type, _refs = references)
48
+ params = []
49
+
50
+ props_by_type(type).each do |prop|
51
+ desc = describe_property(prop)
52
+
53
+ # Generic array of scalars
54
+ next params << { prop => [] } if desc[:type] == :array
55
+
56
+ # Otherwise prop and param are equivalent
57
+ # We also accept the ref name as the foreign key if its a singular resource
58
+ params << prop
59
+ end
60
+
61
+ # Allow id in if its an update so we can find the original record
62
+ params << identifier_property if type == "update"
63
+
64
+ params
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rhino
4
+ module Resource
5
+ module ActiveModelExtension
6
+ module Properties # rubocop:disable Metrics/ModuleLength
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do # rubocop:disable Metrics/BlockLength
10
+ def identifier_property
11
+ "id"
12
+ end
13
+
14
+ def readable_properties
15
+ props = attribute_names - foreign_key_properties
16
+
17
+ props.concat(reference_properties)
18
+
19
+ props.map(&:to_s)
20
+ end
21
+
22
+ def creatable_properties
23
+ writeable_properties
24
+ end
25
+
26
+ def updatable_properties
27
+ writeable_properties
28
+ end
29
+
30
+ def describe_property(property) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
31
+ name = property_name(property).to_s
32
+ {
33
+ "x-rhino-attribute": {
34
+ name:,
35
+ readableName: name.titleize,
36
+ readable: read_properties.include?(property),
37
+ creatable: create_properties.include?(property),
38
+ updatable: update_properties.include?(property)
39
+ },
40
+ readOnly: property_read_only?(name),
41
+ writeOnly: property_write_only?(name),
42
+ nullable: property_nullable?(name),
43
+ default: property_default(name)
44
+ }
45
+ .merge(property_type(property))
46
+ .merge(property_validations(property))
47
+ .compact
48
+ end
49
+
50
+ private
51
+ # FIXME: Duplicated in params.rb
52
+ def reference_to_sym(reference)
53
+ reference.is_a?(Hash) ? reference.keys.first : reference
54
+ end
55
+
56
+ def automatic_properties
57
+ [identifier_property]
58
+ end
59
+
60
+ def foreign_key_properties
61
+ # reflect_on_all_associations(:belongs_to).map(&:foreign_key).map(&:to_s)
62
+ []
63
+ end
64
+
65
+ def reference_properties(read = true) # rubocop:todo Style/OptionalBooleanParameter
66
+ references.filter_map do |r|
67
+ sym = reference_to_sym(r)
68
+
69
+ # All references are readable
70
+ next sym if read
71
+
72
+ # Writeable if a one type or accepting nested
73
+ association = reflect_on_association(sym)
74
+ # rubocop:todo Performance/CollectionLiteralInLoop
75
+ sym if %i[has_one belongs_to].include?(association.macro) || nested_attributes_options.key?(sym)
76
+ # rubocop:enable Performance/CollectionLiteralInLoop
77
+ end
78
+ end
79
+
80
+ def writeable_properties
81
+ # Direct properties for this model
82
+ props = attribute_names - automatic_properties - foreign_key_properties
83
+
84
+ props.concat(reference_properties(false))
85
+
86
+ props.map(&:to_s)
87
+ end
88
+
89
+ def property_name(property)
90
+ property.is_a?(Hash) ? property.keys.first : property
91
+ end
92
+
93
+ def ref_descriptor(name)
94
+ {
95
+ type: :reference,
96
+ anyOf: [
97
+ { :$ref => "#/components/schemas/#{name.singularize}" }
98
+ ]
99
+ }
100
+ end
101
+
102
+ def property_type_raw(property)
103
+ name = property_name(property)
104
+ return :identifier if name == identifier_property
105
+
106
+ # return :string if defined_enums.key?(name)
107
+
108
+ # FIXME: Hack for tags for now
109
+ # if attribute_types.key?(name.to_s) && attribute_types[name.to_s].class.to_s == 'ActsAsTaggableOn::Taggable::TagListType'
110
+ # return {
111
+ # type: :array,
112
+ # items: {
113
+ # type: 'string'
114
+ # }
115
+ # }
116
+ # end
117
+
118
+ # Use the attribute type if possible
119
+ return attribute_types[name.to_s].type if attribute_types.key?(name.to_s)
120
+
121
+ # if reflections.key?(name)
122
+ # # FIXME: The tr hack is to match how model_name in rails handles modularized classes
123
+ # class_name = reflections[name].options[:class_name]&.underscore&.tr('/', '_') || name
124
+ # return ref_descriptor(class_name) unless reflections[name].macro == :has_many
125
+ #
126
+ # return {
127
+ # type: :array,
128
+ # items: ref_descriptor(class_name)
129
+ # }
130
+ # end
131
+
132
+ # raise UnknownpropertyType
133
+ "unknown"
134
+ end
135
+
136
+ def property_type(property)
137
+ pt = property_type_raw(property)
138
+
139
+ return pt if pt.is_a? Hash
140
+
141
+ { type: property_type_raw(property) }
142
+ end
143
+
144
+ # rubocop:todo Metrics/PerceivedComplexity
145
+ def property_validations(property) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
146
+ constraint_hash = {}
147
+
148
+ # https://swagger.io/specification/
149
+
150
+ validators_on(property).each do |v|
151
+ if v.is_a? ActiveModel::Validations::NumericalityValidator
152
+ if v.options.key?(:greater_than)
153
+ constraint_hash[:minimum] = v.options[:greater_than]
154
+ constraint_hash[:exclusiveMinimum] = true
155
+ end
156
+
157
+ if v.options.key?(:less_than)
158
+ constraint_hash[:maximum] = v.options[:less_than]
159
+ constraint_hash[:exclusiveMaximum] = true
160
+ end
161
+
162
+ constraint_hash[:minimum] = v.options[:greater_than_or_equal_to] if v.options.key?(:greater_than_or_equal_to)
163
+ constraint_hash[:maximum] = v.options[:less_than_or_equal_to] if v.options.key?(:less_than_or_equal_to)
164
+
165
+ if v.options.key?(:in)
166
+ constraint_hash[:minimum] = v.options[:in].min
167
+ constraint_hash[:maximum] = v.options[:in].max
168
+ end
169
+ end
170
+
171
+ if v.is_a? ::ActiveModel::Validations::LengthValidator
172
+ constraint_hash[:minLength] = v.options[:minimum] || v.options[:is]
173
+ constraint_hash[:maxLength] = v.options[:maximum] || v.options[:is]
174
+ end
175
+
176
+ constraint_hash[:pattern] = JsRegex.new(v.options[:with]).source if v.is_a? ::ActiveModel::Validations::FormatValidator
177
+
178
+ constraint_hash[:enum] = v.options[:in] if v.is_a? ActiveModel::Validations::InclusionValidator
179
+ end
180
+
181
+ constraint_hash.compact
182
+ end
183
+ # rubocop:enable Metrics/PerceivedComplexity
184
+
185
+ # If there is a presence validator in the model it is not nullable.
186
+ # if there is no optional: true on an association, rails will add a
187
+ # presence validator automatically
188
+ # Otherwise check the db for the actual column or foreign key setting
189
+ # Return nil instead of false for compaction
190
+ def property_nullable?(name)
191
+ # Check for presence validator
192
+ if validators.select { |v| v.is_a? ::ActiveModel::Validations::PresenceValidator }.flat_map(&:attributes).include?(name.to_sym)
193
+ return false
194
+ end
195
+
196
+ # By default, numericality doesn't allow nil values. You can use allow_nil: true option to permit it.
197
+ validators_on(name).select { |v| v.is_a? ::ActiveModel::Validations::NumericalityValidator }.each do |v|
198
+ return false unless v.options[:allow_nil]
199
+ end
200
+
201
+ true
202
+ end
203
+
204
+ # Return nil instead of false for compaction
205
+ def property_read_only?(name)
206
+ return unless read_properties.include?(name) && (create_properties.exclude?(name) && update_properties.exclude?(name))
207
+
208
+ true
209
+ end
210
+
211
+ # Return nil instead of false for compaction
212
+ def property_write_only?(name)
213
+ return unless (create_properties.include?(name) || update_properties.include?(name)) && read_properties.exclude?(name)
214
+
215
+ true
216
+ end
217
+
218
+ def property_default(name)
219
+ # FIXME: This will not handle datetime fields
220
+ # https://github.com/rails/rails/issues/27077 sets the default in the db
221
+ # but Blog.new does not set the default value like other attributes
222
+ # https://nubinary.atlassian.net/browse/NUB-298
223
+ _default_attributes[name].type_cast(_default_attributes[name].value_before_type_cast)
224
+ end
225
+ end
226
+ end
227
+ end
228
+ end
229
+ end