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.
- checksums.yaml +7 -0
- data/README.md +28 -0
- data/Rakefile +34 -0
- data/app/assets/stripe_flow.png +0 -0
- data/app/controllers/concerns/rhino/authenticated.rb +18 -0
- data/app/controllers/concerns/rhino/error_handling.rb +58 -0
- data/app/controllers/concerns/rhino/paper_trail_whodunnit.rb +11 -0
- data/app/controllers/concerns/rhino/permit.rb +38 -0
- data/app/controllers/concerns/rhino/set_current_user.rb +13 -0
- data/app/controllers/rhino/account_controller.rb +34 -0
- data/app/controllers/rhino/active_model_extension_controller.rb +52 -0
- data/app/controllers/rhino/base_controller.rb +23 -0
- data/app/controllers/rhino/crud_controller.rb +57 -0
- data/app/controllers/rhino/simple_controller.rb +11 -0
- data/app/controllers/rhino/simple_stream_controller.rb +12 -0
- data/app/helpers/rhino/omniauth_helper.rb +67 -0
- data/app/helpers/rhino/policy_helper.rb +42 -0
- data/app/helpers/rhino/segment_helper.rb +62 -0
- data/app/models/rhino/account.rb +13 -0
- data/app/models/rhino/current.rb +7 -0
- data/app/models/rhino/user.rb +44 -0
- data/app/overrides/active_record/autosave_association_override.rb +18 -0
- data/app/overrides/active_record/delegated_type_override.rb +14 -0
- data/app/overrides/activestorage/direct_uploads_controller_override.rb +23 -0
- data/app/overrides/activestorage/redirect_controller_override.rb +21 -0
- data/app/overrides/activestorage/redirect_representation_controller_override.rb +21 -0
- data/app/overrides/devise_token_auth/confirmations_controller_override.rb +14 -0
- data/app/overrides/devise_token_auth/omniauth_callbacks_controller_override.rb +45 -0
- data/app/overrides/devise_token_auth/passwords_controller_override.rb +9 -0
- data/app/overrides/devise_token_auth/registrations_controller_override.rb +20 -0
- data/app/overrides/devise_token_auth/sessions_controller_override.rb +26 -0
- data/app/overrides/devise_token_auth/token_validations_controller_override.rb +18 -0
- data/app/policies/rhino/account_policy.rb +27 -0
- data/app/policies/rhino/active_storage_attachment_policy.rb +16 -0
- data/app/policies/rhino/admin_policy.rb +20 -0
- data/app/policies/rhino/base_policy.rb +72 -0
- data/app/policies/rhino/crud_policy.rb +109 -0
- data/app/policies/rhino/editor_policy.rb +12 -0
- data/app/policies/rhino/global_policy.rb +8 -0
- data/app/policies/rhino/resource_info_policy.rb +9 -0
- data/app/policies/rhino/user_policy.rb +20 -0
- data/app/policies/rhino/viewer_policy.rb +19 -0
- data/app/resources/rhino/info_graph.rb +41 -0
- data/app/resources/rhino/open_api_info.rb +108 -0
- data/config/routes.rb +19 -0
- data/db/migrate/20180101000000_devise_token_auth_create_users.rb +54 -0
- data/db/migrate/20180622142754_add_allow_change_password_to_users.rb +5 -0
- data/db/migrate/20191217010224_add_approved_to_users.rb +7 -0
- data/db/migrate/20200503182019_change_tokens_to_json_b.rb +9 -0
- data/lib/commands/rhino_command.rb +59 -0
- data/lib/generators/rhino/dev/setup/setup_generator.rb +175 -0
- data/lib/generators/rhino/dev/setup/templates/env.client.tt +4 -0
- data/lib/generators/rhino/dev/setup/templates/env.server.tt +35 -0
- data/lib/generators/rhino/dev/setup/templates/prepare-commit-msg +17 -0
- data/lib/generators/rhino/install/install_generator.rb +24 -0
- data/lib/generators/rhino/install/templates/account.rb +4 -0
- data/lib/generators/rhino/install/templates/rhino.rb +24 -0
- data/lib/generators/rhino/install/templates/user.rb +4 -0
- data/lib/generators/rhino/module/module_generator.rb +93 -0
- data/lib/generators/rhino/module/templates/engine.rb.tt +19 -0
- data/lib/generators/rhino/module/templates/install_generator.rb.tt +12 -0
- data/lib/generators/rhino/module/templates/module_tasks.rake.tt +17 -0
- data/lib/generators/rhino/policy/policy_generator.rb +33 -0
- data/lib/generators/rhino/policy/templates/policy.rb.tt +46 -0
- data/lib/generators/test_unit/rhino_policy_generator.rb +13 -0
- data/lib/generators/test_unit/templates/policy_test.rb.tt +57 -0
- data/lib/rhino/engine.rb +140 -0
- data/lib/rhino/omniauth/strategies/azure_o_auth2.rb +16 -0
- data/lib/rhino/resource/active_model_extension/backing_store/google_sheet.rb +89 -0
- data/lib/rhino/resource/active_model_extension/backing_store.rb +33 -0
- data/lib/rhino/resource/active_model_extension/describe.rb +38 -0
- data/lib/rhino/resource/active_model_extension/params.rb +70 -0
- data/lib/rhino/resource/active_model_extension/properties.rb +229 -0
- data/lib/rhino/resource/active_model_extension/reference.rb +50 -0
- data/lib/rhino/resource/active_model_extension/routing.rb +15 -0
- data/lib/rhino/resource/active_model_extension/serialization.rb +16 -0
- data/lib/rhino/resource/active_model_extension.rb +38 -0
- data/lib/rhino/resource/active_record_extension/describe.rb +44 -0
- data/lib/rhino/resource/active_record_extension/params.rb +213 -0
- data/lib/rhino/resource/active_record_extension/properties.rb +85 -0
- data/lib/rhino/resource/active_record_extension/properties_describe.rb +226 -0
- data/lib/rhino/resource/active_record_extension/reference.rb +50 -0
- data/lib/rhino/resource/active_record_extension/routing.rb +21 -0
- data/lib/rhino/resource/active_record_extension/search.rb +23 -0
- data/lib/rhino/resource/active_record_extension/serialization.rb +16 -0
- data/lib/rhino/resource/active_record_extension.rb +30 -0
- data/lib/rhino/resource/active_record_tree.rb +50 -0
- data/lib/rhino/resource/active_storage_extension.rb +41 -0
- data/lib/rhino/resource/describe.rb +19 -0
- data/lib/rhino/resource/owner.rb +172 -0
- data/lib/rhino/resource/params.rb +31 -0
- data/lib/rhino/resource/properties.rb +192 -0
- data/lib/rhino/resource/reference.rb +31 -0
- data/lib/rhino/resource/routing.rb +107 -0
- data/lib/rhino/resource/serialization.rb +13 -0
- data/lib/rhino/resource/sieves.rb +36 -0
- data/lib/rhino/resource.rb +54 -0
- data/lib/rhino/sieve/filter.rb +149 -0
- data/lib/rhino/sieve/helpers.rb +11 -0
- data/lib/rhino/sieve/limit.rb +20 -0
- data/lib/rhino/sieve/offset.rb +16 -0
- data/lib/rhino/sieve/order.rb +143 -0
- data/lib/rhino/sieve/search.rb +20 -0
- data/lib/rhino/sieve.rb +158 -0
- data/lib/rhino/test_case/controller.rb +134 -0
- data/lib/rhino/test_case/override.rb +19 -0
- data/lib/rhino/test_case/policy.rb +76 -0
- data/lib/rhino/test_case.rb +10 -0
- data/lib/rhino/version.rb +17 -0
- data/lib/rhino.rb +129 -0
- data/lib/tasks/rhino.rake +38 -0
- data/lib/tasks/rhino_dev.rake +17 -0
- data/lib/validators/country_validator.rb +11 -0
- data/lib/validators/email_validator.rb +8 -0
- data/lib/validators/ipv4_validator.rb +10 -0
- data/lib/validators/mac_address_validator.rb +9 -0
- 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
|
data/lib/rhino/engine.rb
ADDED
@@ -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
|