forest_liana 6.3.8 → 7.0.0.beta.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/forest_liana/actions_controller.rb +44 -29
  3. data/app/controllers/forest_liana/application_controller.rb +2 -2
  4. data/app/controllers/forest_liana/associations_controller.rb +1 -1
  5. data/app/controllers/forest_liana/base_controller.rb +1 -1
  6. data/app/controllers/forest_liana/resources_controller.rb +6 -6
  7. data/app/serializers/forest_liana/intercom_attribute_serializer.rb +1 -1
  8. data/app/serializers/forest_liana/intercom_conversation_serializer.rb +1 -1
  9. data/app/serializers/forest_liana/mixpanel_event_serializer.rb +1 -1
  10. data/app/serializers/forest_liana/serializer_factory.rb +1 -1
  11. data/app/serializers/forest_liana/stat_serializer.rb +1 -1
  12. data/app/serializers/forest_liana/stripe_bank_account_serializer.rb +1 -1
  13. data/app/serializers/forest_liana/stripe_card_serializer.rb +1 -1
  14. data/app/serializers/forest_liana/stripe_invoice_serializer.rb +1 -1
  15. data/app/serializers/forest_liana/stripe_payment_serializer.rb +1 -1
  16. data/app/serializers/forest_liana/stripe_subscription_serializer.rb +1 -1
  17. data/app/services/forest_liana/apimap_sorter.rb +1 -0
  18. data/app/services/forest_liana/smart_action_field_validator.rb +49 -0
  19. data/config/initializers/errors.rb +17 -0
  20. data/config/initializers/logger.rb +16 -13
  21. data/lib/forest_liana.rb +2 -0
  22. data/lib/forest_liana/bootstrapper.rb +2 -2
  23. data/lib/forest_liana/schema_file_updater.rb +8 -0
  24. data/lib/forest_liana/version.rb +1 -1
  25. data/spec/config/initializers/logger_spec.rb +30 -0
  26. data/spec/lib/forest_liana/schema_file_updater_spec.rb +94 -0
  27. data/spec/requests/actions_controller_spec.rb +22 -20
  28. data/spec/services/forest_liana/smart_action_field_validator_spec.rb +70 -0
  29. metadata +12 -8
  30. data/app/helpers/forest_liana/is_same_data_structure_helper.rb +0 -44
  31. data/spec/helpers/forest_liana/is_same_data_structure_helper_spec.rb +0 -87
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e0fd5ef4e67fd45b6a701da7dbf8da36d6f6abb3d9e2be4d0031d0495a10727a
4
- data.tar.gz: 9c823060491af5ff42f098e041ffe9f2ac672865c4a790e4b8bb3f1bb1e7447b
3
+ metadata.gz: 6be4888e921b11142c5bc7774e6a0c9ce9a91b7e0b8ceb8a812cff8e297b540a
4
+ data.tar.gz: '043584e75c96d49b34a4ab6ec0e0f0315700eb86a7d061fb03de66c095840b1c'
5
5
  SHA512:
6
- metadata.gz: c6a0639f27ebecd6f7e66e83bbdc527a08e0483435e6485c80ed66f45832c886ef50cb46402a4b74b1ff8cc6b8f284419f53fd1ebebf532c57fe089d2d0bdec6
7
- data.tar.gz: 0e329ab499ed3b2414c50097f3b2cad74733c0bc51db835b25bb1a9713229f8feb6f42ad1b7fa3a038f73e357dc46bc3cad0fc6f6ad61224f306c644d02fbf1e
6
+ metadata.gz: bfc23dfba08023225f17e44860caf95dc2fe041bcf81a1d61651ee96cea59563aa2c97ee6a1bcdc2e38e2e56a0f9c7c5e144b64583286af70864ec4d636a5795
7
+ data.tar.gz: 2cc2bf17cc1a235264db9981e33e99b22c23468514bf136e3379523020ca1efd906b792b46c45b6893f0edb279d8845c79e2e14ee8821ee47db6fc87d4992692
@@ -27,34 +27,45 @@ module ForestLiana
27
27
  end
28
28
 
29
29
  def get_smart_action_load_ctx(fields)
30
- fields = fields.reduce({}) do |p, c|
31
- ForestLiana::WidgetsHelper.set_field_widget(c)
32
- p.update(c[:field] => c.merge!(value: nil))
30
+ fields = fields.map do |field|
31
+ ForestLiana::WidgetsHelper.set_field_widget(field)
32
+ field[:value] = nil unless field[:value]
33
+ field
33
34
  end
34
35
  {:record => get_record, :fields => fields}
35
36
  end
36
37
 
37
- def get_smart_action_change_ctx(fields)
38
- fields = fields.reduce({}) do |p, c|
39
- field = c.permit!.to_h.symbolize_keys
38
+ def get_smart_action_change_ctx(fields, field_changed)
39
+ found_field_changed = fields.find{|field| field[:field] == field_changed}
40
+ fields = fields.map do |field|
41
+ field = field.permit!.to_h.symbolize_keys
40
42
  ForestLiana::WidgetsHelper.set_field_widget(field)
41
- p.update(c[:field] => field)
43
+ field
42
44
  end
43
- {:record => get_record, :fields => fields}
45
+ {:record => get_record, :field_changed => found_field_changed, :fields => fields}
44
46
  end
45
47
 
46
- def handle_result(result, formatted_fields, action)
47
- if result.nil? || !result.is_a?(Hash)
48
- return render status: 500, json: { error: 'Error in smart action load hook: hook must return an object' }
48
+ def handle_result(result, action)
49
+ if result.nil? || !result.is_a?(Array)
50
+ return render status: 500, json: { error: 'Error in smart action load hook: hook must return an array of fields' }
49
51
  end
50
- is_same_data_structure = ForestLiana::IsSameDataStructureHelper::Analyser.new(formatted_fields, result, 1)
51
- unless is_same_data_structure.perform
52
- return render status: 500, json: { error: 'Error in smart action hook: fields must be unchanged (no addition nor deletion allowed)' }
52
+
53
+ # Validate that the fields are well formed.
54
+ begin
55
+ # action.hooks[:change] is a hashmap here
56
+ # to do the validation, only the hook names are require
57
+ change_hooks_name = action.hooks[:change].nil? ? nil : action.hooks[:change].keys
58
+ ForestLiana::SmartActionFieldValidator.validate_smart_action_fields(result, action.name, change_hooks_name)
59
+ rescue ForestLiana::Errors::SmartActionInvalidFieldError => invalid_field_error
60
+ FOREST_LOGGER.warn invalid_field_error.message
61
+ rescue ForestLiana::Errors::SmartActionInvalidFieldHookError => invalid_hook_error
62
+ FOREST_LOGGER.error invalid_hook_error.message
63
+ return render status: 500, json: { error: invalid_hook_error.message }
53
64
  end
54
65
 
55
66
  # Apply result on fields (transform the object back to an array), preserve order.
56
- fields = action.fields.map do |field|
57
- updated_field = result[field[:field]]
67
+ fields = result.map do |field|
68
+ updated_field = result.find{|f| f[:field] == field[:field]}
58
69
 
59
70
  # Reset `value` when not present in `enums` (which means `enums` has changed).
60
71
  if updated_field[:enums].is_a?(Array)
@@ -72,7 +83,7 @@ module ForestLiana
72
83
  updated_field
73
84
  end
74
85
 
75
- render serializer: nil, json: { fields: fields}, status: :ok
86
+ render serializer: nil, json: { fields: fields }, status: :ok
76
87
  end
77
88
 
78
89
  def load
@@ -81,14 +92,13 @@ module ForestLiana
81
92
  if !action
82
93
  render status: 500, json: {error: 'Error in smart action load hook: cannot retrieve action from collection'}
83
94
  else
84
- # Transform fields from array to an object to ease usage in hook, adds null value.
95
+ # Get the smart action hook load context
85
96
  context = get_smart_action_load_ctx(action.fields)
86
- formatted_fields = context[:fields].clone # clone for following test on is_same_data_structure
87
97
 
88
98
  # Call the user-defined load hook.
89
99
  result = action.hooks[:load].(context)
90
100
 
91
- handle_result(result, formatted_fields, action)
101
+ handle_result(result, action)
92
102
  end
93
103
  end
94
104
 
@@ -96,17 +106,22 @@ module ForestLiana
96
106
  action = get_action(params[:collectionName])
97
107
 
98
108
  if !action
99
- render status: 500, json: {error: 'Error in smart action change hook: cannot retrieve action from collection'}
100
- else
101
- # Transform fields from array to an object to ease usage in hook.
102
- context = get_smart_action_change_ctx(params[:fields])
103
- formatted_fields = context[:fields].clone # clone for following test on is_same_data_structure
109
+ return render status: 500, json: {error: 'Error in smart action change hook: cannot retrieve action from collection'}
110
+ elsif params[:fields].nil?
111
+ return render status: 500, json: {error: 'Error in smart action change hook: fields params is mandatory'}
112
+ elsif !params[:fields].is_a?(Array)
113
+ return render status: 500, json: {error: 'Error in smart action change hook: fields params must be an array'}
114
+ end
104
115
 
105
- # Call the user-defined change hook.
106
- result = action.hooks[:change][params[:changedField]].(context)
116
+ # Get the smart action hook change context
117
+ context = get_smart_action_change_ctx(params[:fields], params[:changedField])
107
118
 
108
- handle_result(result, formatted_fields, action)
109
- end
119
+ field_changed_hook = context[:field_changed][:hook]
120
+
121
+ # Call the user-defined change hook.
122
+ result = action.hooks[:change][field_changed_hook].(context)
123
+
124
+ handle_result(result, action)
110
125
  end
111
126
  end
112
127
  end
@@ -34,14 +34,14 @@ module ForestLiana
34
34
 
35
35
  def serialize_model(record, options = {})
36
36
  options[:is_collection] = false
37
- json = JSONAPI::Serializer.serialize(record, options)
37
+ json = ForestAdmin::JSONAPI::Serializer.serialize(record, options)
38
38
 
39
39
  force_utf8_encoding(json)
40
40
  end
41
41
 
42
42
  def serialize_models(records, options = {}, fields_searched = [])
43
43
  options[:is_collection] = true
44
- json = JSONAPI::Serializer.serialize(records, options)
44
+ json = ForestAdmin::JSONAPI::Serializer.serialize(records, options)
45
45
 
46
46
  if options[:params] && options[:params][:search]
47
47
  # NOTICE: Add the Smart Fields with a 'String' type.
@@ -41,7 +41,7 @@ module ForestLiana
41
41
  updater.perform
42
42
 
43
43
  if updater.errors
44
- render serializer: nil, json: JSONAPI::Serializer.serialize_errors(
44
+ render serializer: nil, json: ForestAdmin::JSONAPI::Serializer.serialize_errors(
45
45
  updater.errors), status: 422
46
46
  else
47
47
  head :no_content
@@ -24,7 +24,7 @@ module ForestLiana
24
24
  end
25
25
  end
26
26
  rescue ForestLiana::Errors::ExpectedError => exception
27
- error_data = JSONAPI::Serializer.serialize_errors([{
27
+ error_data = ForestAdmin::JSONAPI::Serializer.serialize_errors([{
28
28
  status: exception.error_code,
29
29
  detail: exception.message
30
30
  }])
@@ -41,7 +41,7 @@ module ForestLiana
41
41
  status: :unprocessable_entity, serializer: nil
42
42
  rescue ForestLiana::Errors::ExpectedError => error
43
43
  error.display_error
44
- error_data = JSONAPI::Serializer.serialize_errors([{
44
+ error_data = ForestAdmin::JSONAPI::Serializer.serialize_errors([{
45
45
  status: error.error_code,
46
46
  detail: error.message
47
47
  }])
@@ -73,7 +73,7 @@ module ForestLiana
73
73
  status: :unprocessable_entity, serializer: nil
74
74
  rescue ForestLiana::Errors::ExpectedError => error
75
75
  error.display_error
76
- error_data = JSONAPI::Serializer.serialize_errors([{
76
+ error_data = ForestAdmin::JSONAPI::Serializer.serialize_errors([{
77
77
  status: error.error_code,
78
78
  detail: error.message
79
79
  }])
@@ -108,12 +108,12 @@ module ForestLiana
108
108
  creator.perform
109
109
 
110
110
  if creator.errors
111
- render serializer: nil, json: JSONAPI::Serializer.serialize_errors(
111
+ render serializer: nil, json: ForestAdmin::JSONAPI::Serializer.serialize_errors(
112
112
  creator.errors), status: 400
113
113
  elsif creator.record.valid?
114
114
  render serializer: nil, json: render_record_jsonapi(creator.record)
115
115
  else
116
- render serializer: nil, json: JSONAPI::Serializer.serialize_errors(
116
+ render serializer: nil, json: ForestAdmin::JSONAPI::Serializer.serialize_errors(
117
117
  creator.record.errors), status: 400
118
118
  end
119
119
  rescue => error
@@ -131,12 +131,12 @@ module ForestLiana
131
131
  updater.perform
132
132
 
133
133
  if updater.errors
134
- render serializer: nil, json: JSONAPI::Serializer.serialize_errors(
134
+ render serializer: nil, json: ForestAdmin::JSONAPI::Serializer.serialize_errors(
135
135
  updater.errors), status: 400
136
136
  elsif updater.record.valid?
137
137
  render serializer: nil, json: render_record_jsonapi(updater.record)
138
138
  else
139
- render serializer: nil, json: JSONAPI::Serializer.serialize_errors(
139
+ render serializer: nil, json: ForestAdmin::JSONAPI::Serializer.serialize_errors(
140
140
  updater.record.errors), status: 400
141
141
  end
142
142
  rescue => error
@@ -1,6 +1,6 @@
1
1
  module ForestLiana
2
2
  class IntercomAttributeSerializer
3
- include JSONAPI::Serializer
3
+ include ForestAdmin::JSONAPI::Serializer
4
4
 
5
5
  attribute :session_count
6
6
  attribute :last_seen_ip
@@ -1,6 +1,6 @@
1
1
  module ForestLiana
2
2
  class IntercomConversationSerializer
3
- include JSONAPI::Serializer
3
+ include ForestAdmin::JSONAPI::Serializer
4
4
 
5
5
  attribute :created_at
6
6
  attribute :updated_at
@@ -1,6 +1,6 @@
1
1
  module ForestLiana
2
2
  class MixpanelEventSerializer
3
- include JSONAPI::Serializer
3
+ include ForestAdmin::JSONAPI::Serializer
4
4
 
5
5
  attribute :id
6
6
  attribute :event
@@ -53,7 +53,7 @@ module ForestLiana
53
53
 
54
54
  def serializer_for(active_record_class)
55
55
  serializer = Class.new {
56
- include JSONAPI::Serializer
56
+ include ForestAdmin::JSONAPI::Serializer
57
57
 
58
58
  def self_link
59
59
  "/forest#{super.underscore}"
@@ -1,6 +1,6 @@
1
1
  module ForestLiana
2
2
  class StatSerializer
3
- include JSONAPI::Serializer
3
+ include ForestAdmin::JSONAPI::Serializer
4
4
 
5
5
  attribute :value
6
6
 
@@ -1,6 +1,6 @@
1
1
  module ForestLiana
2
2
  class StripeBankAccountSerializer
3
- include JSONAPI::Serializer
3
+ include ForestAdmin::JSONAPI::Serializer
4
4
 
5
5
  attribute :account_holder_name
6
6
  attribute :account_holder_type
@@ -1,6 +1,6 @@
1
1
  module ForestLiana
2
2
  class StripeCardSerializer
3
- include JSONAPI::Serializer
3
+ include ForestAdmin::JSONAPI::Serializer
4
4
 
5
5
  attribute :last4
6
6
  attribute :brand
@@ -1,6 +1,6 @@
1
1
  module ForestLiana
2
2
  class StripeInvoiceSerializer
3
- include JSONAPI::Serializer
3
+ include ForestAdmin::JSONAPI::Serializer
4
4
 
5
5
  attribute :amount_due
6
6
  attribute :amount_paid
@@ -1,6 +1,6 @@
1
1
  module ForestLiana
2
2
  class StripePaymentSerializer
3
- include JSONAPI::Serializer
3
+ include ForestAdmin::JSONAPI::Serializer
4
4
 
5
5
  attribute :description
6
6
  attribute :refunded
@@ -1,6 +1,6 @@
1
1
  module ForestLiana
2
2
  class StripeSubscriptionSerializer
3
- include JSONAPI::Serializer
3
+ include ForestAdmin::JSONAPI::Serializer
4
4
 
5
5
  attribute :cancel_at_period_end
6
6
  attribute :canceled_at
@@ -52,6 +52,7 @@ module ForestLiana
52
52
  'description',
53
53
  'position',
54
54
  'widget',
55
+ 'hook',
55
56
  ]
56
57
  KEYS_SEGMENT = ['name']
57
58
 
@@ -0,0 +1,49 @@
1
+ module ForestLiana
2
+ class SmartActionFieldValidator
3
+
4
+ @@accepted_primitive_field_type = [
5
+ 'String',
6
+ 'Number',
7
+ 'Date',
8
+ 'Boolean',
9
+ 'File',
10
+ 'Enum',
11
+ 'Json',
12
+ 'Dateonly',
13
+ ]
14
+
15
+ @@accepted_array_field_type = [
16
+ 'String',
17
+ 'Number',
18
+ 'Date',
19
+ 'boolean',
20
+ 'File',
21
+ 'Enum',
22
+ ]
23
+
24
+ def self.validate_field(field, action_name)
25
+ raise ForestLiana::Errors::SmartActionInvalidFieldError.new(action_name, nil, "The field attribute must be defined") if !field || field[:field].nil?
26
+ raise ForestLiana::Errors::SmartActionInvalidFieldError.new(action_name, nil, "The field attribute must be a string.") if !field[:field].is_a?(String)
27
+ raise ForestLiana::Errors::SmartActionInvalidFieldError.new(action_name, field[:field], "The description attribute must be a string.") if field[:description] && !field[:description].is_a?(String)
28
+ raise ForestLiana::Errors::SmartActionInvalidFieldError.new(action_name, field[:field], "The enums attribute must be an array.") if field[:enums] && !field[:enums].is_a?(Array)
29
+ raise ForestLiana::Errors::SmartActionInvalidFieldError.new(action_name, field[:field], "The reference attribute must be a string.") if field[:reference] && !field[:reference].is_a?(String)
30
+
31
+ is_type_valid = field[:type].is_a?(Array) ?
32
+ @@accepted_array_field_type.include?(field[:type][0]) :
33
+ @@accepted_primitive_field_type.include?(field[:type])
34
+
35
+ raise ForestLiana::Errors::SmartActionInvalidFieldError.new(action_name, field[:field], "The type attribute must be a valid type. See the documentation for more information. https://docs.forestadmin.com/documentation/reference-guide/fields/create-and-manage-smart-fields#available-field-options.") if !is_type_valid
36
+ end
37
+
38
+ def self.validate_field_change_hook(field, action_name, hooks)
39
+ raise ForestLiana::Errors::SmartActionInvalidFieldHookError.new(action_name, field[:field], field[:hook]) if field[:hook] && !hooks.find{|hook| hook == field[:hook]}
40
+ end
41
+
42
+ def self.validate_smart_action_fields(fields, action_name, change_hooks)
43
+ fields.map{|field|
44
+ self.validate_field(field.symbolize_keys, action_name)
45
+ self.validate_field_change_hook(field.symbolize_keys, action_name, change_hooks) if change_hooks
46
+ }
47
+ end
48
+ end
49
+ end
@@ -13,6 +13,23 @@ module ForestLiana
13
13
  end
14
14
  end
15
15
 
16
+ class SmartActionInvalidFieldError < StandardError
17
+ def initialize(action_name=nil, field_name=nil, message=nil)
18
+ error_message = ""
19
+ error_message << "Error while parsing action \"#{action_name}\"" if !action_name.nil?
20
+ error_message << " on field \"#{field_name}\"" if !field_name.nil?
21
+ error_message << ": " if !field_name.nil? || !action_name.nil?
22
+ error_message << message if !message.nil?
23
+ super(error_message)
24
+ end
25
+ end
26
+
27
+ class SmartActionInvalidFieldHookError < StandardError
28
+ def initialize(action_name=nil, field_name=nil, hook_name=nil)
29
+ super("The hook \"#{hook_name}\" of \"#{field_name}\" field on the smart action \"#{action_name}\" is not defined.")
30
+ end
31
+ end
32
+
16
33
  class ExpectedError < StandardError
17
34
  attr_reader :error_code, :status, :message, :name
18
35
 
@@ -3,21 +3,24 @@ module ForestLiana
3
3
  class Logger
4
4
  class << self
5
5
  def log
6
- logger = ::Logger.new(STDOUT)
7
- logger_colors = {
8
- DEBUG: 34,
9
- WARN: 33,
10
- ERROR: 31,
11
- INFO: 37
12
- }
6
+ if ForestLiana.logger != nil
7
+ logger = ForestLiana.logger
8
+ else
9
+ logger = ::Logger.new(STDOUT)
10
+ logger_colors = {
11
+ DEBUG: 34,
12
+ WARN: 33,
13
+ ERROR: 31,
14
+ INFO: 37
15
+ }
13
16
 
14
- logger.formatter = proc do |severity, datetime, progname, message|
15
- displayed_message = "[#{datetime.to_s(:db)}] Forest 🌳🌳🌳 " \
16
- "#{message}\n"
17
- "\e[#{logger_colors[severity.to_sym]}m#{displayed_message}\033[0m"
17
+ logger.formatter = proc do |severity, datetime, progname, message|
18
+ displayed_message = "[#{datetime.to_s(:db)}] Forest 🌳🌳🌳 " \
19
+ "#{message}\n"
20
+ "\e[#{logger_colors[severity.to_sym]}m#{displayed_message}\033[0m"
21
+ end
22
+ logger
18
23
  end
19
-
20
- logger
21
24
  end
22
25
  end
23
26
  end
data/lib/forest_liana.rb CHANGED
@@ -27,6 +27,7 @@ module ForestLiana
27
27
  mattr_accessor :user_class_name
28
28
  mattr_accessor :names_overriden
29
29
  mattr_accessor :meta
30
+ mattr_accessor :logger
30
31
  # TODO: Remove once lianas prior to 2.0.0 are not supported anymore.
31
32
  mattr_accessor :names_old_overriden
32
33
 
@@ -38,6 +39,7 @@ module ForestLiana
38
39
  self.user_class_name = nil
39
40
  self.names_overriden = {}
40
41
  self.meta = {}
42
+ self.logger = nil
41
43
 
42
44
  @config_dir = 'lib/forest_liana/**/*.rb'
43
45
 
@@ -60,7 +60,7 @@ module ForestLiana
60
60
  a = get_action(c, action['name'])
61
61
  load = !a.hooks.nil? && a.hooks.key?(:load) && a.hooks[:load].is_a?(Proc)
62
62
  change = !a.hooks.nil? && a.hooks.key?(:change) && a.hooks[:change].is_a?(Hash) ? a.hooks[:change].keys : []
63
- action['hooks'] = {:load => load, :change => change}
63
+ action['hooks'] = {'load' => load, 'change' => change}
64
64
  end
65
65
  end
66
66
  end
@@ -139,7 +139,7 @@ module ForestLiana
139
139
 
140
140
  # Monkey patch the find_serializer_class_name method to specify the
141
141
  # good serializer to use.
142
- ::JSONAPI::Serializer.class_eval do
142
+ ::ForestAdmin::JSONAPI::Serializer.class_eval do
143
143
  def self.find_serializer_class_name(record, options)
144
144
  if record.respond_to?(:jsonapi_serializer_class_name)
145
145
  record.jsonapi_serializer_class_name.to_s
@@ -63,6 +63,7 @@ module ForestLiana
63
63
  'description',
64
64
  'position',
65
65
  'widget',
66
+ 'hook',
66
67
  ]
67
68
  KEYS_SEGMENT = ['name']
68
69
 
@@ -96,6 +97,13 @@ module ForestLiana
96
97
  end
97
98
 
98
99
  collection['actions'] = collection['actions'].map do |action|
100
+ begin
101
+ SmartActionFieldValidator.validate_smart_action_fields(action['fields'], action['name'], action['hooks']['change'])
102
+ rescue ForestLiana::Errors::SmartActionInvalidFieldError => invalid_field_error
103
+ FOREST_LOGGER.warn invalid_field_error.message
104
+ rescue ForestLiana::Errors::SmartActionInvalidFieldHookError => invalid_hook_error
105
+ FOREST_LOGGER.error invalid_hook_error.message
106
+ end
99
107
  action['fields'] = action['fields'].map { |field| field.slice(*KEYS_ACTION_FIELD) }
100
108
  action.slice(*KEYS_ACTION)
101
109
  end
@@ -1,3 +1,3 @@
1
1
  module ForestLiana
2
- VERSION = "6.3.8"
2
+ VERSION = "7.0.0.beta.2"
3
3
  end
@@ -0,0 +1,30 @@
1
+ module ForestLiana
2
+ describe Logger do
3
+ describe 'self.log' do
4
+ describe 'with a logger overload' do
5
+ it 'should return the given logger' do
6
+ logger = ActiveSupport::Logger.new($stdout)
7
+ logger.formatter = proc do |severity, datetime, progname, msg|
8
+ {:message => msg}.to_json
9
+ end
10
+ ForestLiana.logger = logger
11
+
12
+ expect(Logger.log.is_a?(ActiveSupport::Logger)).to be_truthy
13
+ expect { Logger.log.error "[error] override logger" }.to output({:message => "[error] override logger"}.to_json).to_stdout_from_any_process
14
+ expect { Logger.log.info "[info] override logger" }.to output({:message => "[info] override logger"}.to_json).to_stdout_from_any_process
15
+ end
16
+ end
17
+
18
+ describe 'with no logger overload' do
19
+ it 'should return an instance of ::Logger' do
20
+ ForestLiana.logger = nil
21
+
22
+ expect(Logger.log.is_a?(::Logger)).to be_truthy
23
+ # RegExp is used to check for the forestadmin logger format
24
+ expect { Logger.log.error "[error] default logger" }.to output(/\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\] Forest .* \[error\]/).to_stdout_from_any_process
25
+ expect { Logger.log.info "[info] default logger" }.to output(/\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\] Forest .* \[info\]/).to_stdout_from_any_process
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,94 @@
1
+ module ForestLiana
2
+ describe SchemaFileUpdater do
3
+ describe "initialize" do
4
+ describe "without any collections nor meta" do
5
+ it "should set collections as an empty array and meta as an empty object" do
6
+ schema_file_updater = ForestLiana::SchemaFileUpdater.new("test.txt", [], {})
7
+ expect(schema_file_updater.instance_variable_get(:@collections)).to eq([])
8
+ expect(schema_file_updater.instance_variable_get(:@meta)).to eq({})
9
+ end
10
+ end
11
+
12
+ describe "with a given collection" do
13
+ describe "when the collection has a smart action action" do
14
+ it "should save the smart action" do
15
+ collections = [{
16
+ "fields" => [],
17
+ "actions" => [{
18
+ "fields" => [],
19
+ "name" => "test",
20
+ "hooks" => {
21
+ "change" => []
22
+ }
23
+ }],
24
+ "segments" => []
25
+ }]
26
+ schema_file_updater = ForestLiana::SchemaFileUpdater.new("test.txt", collections, {})
27
+ expect(schema_file_updater.instance_variable_get(:@collections))
28
+ .to eq(collections)
29
+ end
30
+
31
+ describe "when a smart action field is malformed" do
32
+ it "should display a warning message" do
33
+ collections = [{
34
+ "fields" => [],
35
+ "actions" => [{
36
+ "fields" => [{}],
37
+ "name" => "test",
38
+ "hooks" => {
39
+ "change" => []
40
+ }
41
+ }],
42
+ "segments" => []
43
+ }]
44
+ allow(FOREST_LOGGER).to receive(:warn)
45
+ schema_file_updater = ForestLiana::SchemaFileUpdater.new("test.txt", collections, {})
46
+ expect(FOREST_LOGGER).to have_received(:warn).with('Error while parsing action "test": The field attribute must be defined')
47
+ end
48
+ end
49
+
50
+ describe "when a smart action change field hook does not exist" do
51
+ it "should display an error message" do
52
+ collections = [{
53
+ "fields" => [],
54
+ "actions" => [{
55
+ "fields" => [{
56
+ "field" => "testField",
57
+ "hook" => "undefinedHook",
58
+ "type" => "String",
59
+ }],
60
+ "name" => "test",
61
+ "hooks" => {
62
+ "change" => []
63
+ }
64
+ }],
65
+ "segments" => []
66
+ }]
67
+
68
+ allow(FOREST_LOGGER).to receive(:error)
69
+ schema_file_updater = ForestLiana::SchemaFileUpdater.new("test.txt", collections, {})
70
+ expect(FOREST_LOGGER).to have_received(:error).with('The hook "undefinedHook" of "testField" field on the smart action "test" is not defined.')
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ describe "perform" do
78
+ it "should call file puts with pretty printed data" do
79
+ file = instance_double(File, read: "stubbed read")
80
+
81
+ allow(File).to receive(:open).with("test.txt", "w") { |&block| block.call(file) }
82
+
83
+ schema_file_updater = ForestLiana::SchemaFileUpdater.new("test.txt", [], {})
84
+ expected_result = schema_file_updater.pretty_print({
85
+ "collections" => [],
86
+ "meta" => {}
87
+ })
88
+
89
+ expect(file).to receive(:puts).with(expected_result)
90
+ schema_file_updater.perform
91
+ end
92
+ end
93
+ end
94
+ end
@@ -30,6 +30,7 @@ describe 'Requesting Actions routes', :type => :request do
30
30
  reference: nil,
31
31
  description: nil,
32
32
  widget: nil,
33
+ hook: 'on_foo_changed'
33
34
  }
34
35
  enum = {
35
36
  field: 'enum',
@@ -50,10 +51,10 @@ describe 'Requesting Actions routes', :type => :request do
50
51
  context[:fields]
51
52
  },
52
53
  :change => {
53
- 'foo' => -> (context) {
54
- fields = context[:fields]
55
- fields['foo'][:value] = 'baz'
56
- return fields
54
+ 'on_foo_changed' => -> (context) {
55
+ foo = context[:fields].find{|field| field[:field] == 'foo'}
56
+ foo[:value] = 'baz'
57
+ context[:fields]
57
58
  }
58
59
  }
59
60
  }
@@ -66,7 +67,7 @@ describe 'Requesting Actions routes', :type => :request do
66
67
  1
67
68
  },
68
69
  :change => {
69
- 'foo' => -> (context) {
70
+ 'on_foo_changed' => -> (context) {
70
71
  1
71
72
  }
72
73
  }
@@ -77,11 +78,10 @@ describe 'Requesting Actions routes', :type => :request do
77
78
  fields: [foo],
78
79
  hooks: {
79
80
  :load => -> (context) {
80
- context[:fields]['baz'] = foo.clone.update({field: 'baz'})
81
- context[:fields]
81
+ {}
82
82
  },
83
83
  :change => {
84
- 'foo' => -> (context) {
84
+ 'on_foo_changed' => -> (context) {
85
85
  context[:fields]['baz'] = foo.clone.update({field: 'baz'})
86
86
  context[:fields]
87
87
  }
@@ -93,10 +93,11 @@ describe 'Requesting Actions routes', :type => :request do
93
93
  fields: [foo, enum],
94
94
  hooks: {
95
95
  :change => {
96
- 'foo' => -> (context) {
96
+ 'on_foo_changed' => -> (context) {
97
97
  fields = context[:fields]
98
- fields['enum'][:enums] = %w[c d e]
99
- return fields
98
+ enum_field = fields.find{|field| field[:field] == 'enum'}
99
+ enum_field[:enums] = %w[c d e]
100
+ fields
100
101
  }
101
102
  }
102
103
  }
@@ -107,10 +108,11 @@ describe 'Requesting Actions routes', :type => :request do
107
108
  fields: [foo, multiple_enum],
108
109
  hooks: {
109
110
  :change => {
110
- 'foo' => -> (context) {
111
+ 'on_foo_changed' => -> (context) {
111
112
  fields = context[:fields]
112
- fields['multipleEnum'][:enums] = %w[c d z]
113
- return fields
113
+ enum_field = fields.find{|field| field[:field] == 'multipleEnum'}
114
+ enum_field[:enums] = %w[c d z]
115
+ fields
114
116
  }
115
117
  }
116
118
  }
@@ -136,16 +138,19 @@ describe 'Requesting Actions routes', :type => :request do
136
138
  it 'should respond 500 with bad params' do
137
139
  post '/forest/actions/my_action/hooks/load', params: {}
138
140
  expect(response.status).to eq(500)
141
+ expect(JSON.parse(response.body)).to eq({'error' => 'Error in smart action load hook: cannot retrieve action from collection'})
139
142
  end
140
143
 
141
144
  it 'should respond 500 with bad hook result type' do
142
145
  post '/forest/actions/fail_action/hooks/load', params: JSON.dump(params), headers: { 'CONTENT_TYPE' => 'application/json' }
143
146
  expect(response.status).to eq(500)
147
+ expect(JSON.parse(response.body)).to eq({'error' => 'Error in smart action load hook: hook must return an array of fields'})
144
148
  end
145
149
 
146
150
  it 'should respond 500 with bad hook result data structure' do
147
151
  post '/forest/actions/cheat_action/hooks/load', params: JSON.dump(params), headers: { 'CONTENT_TYPE' => 'application/json' }
148
152
  expect(response.status).to eq(500)
153
+ expect(JSON.parse(response.body)).to eq({'error' => 'Error in smart action load hook: hook must return an array of fields'})
149
154
  end
150
155
  end
151
156
 
@@ -163,18 +168,15 @@ describe 'Requesting Actions routes', :type => :request do
163
168
  end
164
169
 
165
170
  it 'should respond 500 with bad params' do
166
- post '/forest/actions/my_action/hooks/change', params: {}
171
+ post '/forest/actions/my_action/hooks/change', params: JSON.dump({collectionName: 'Island'}), headers: { 'CONTENT_TYPE' => 'application/json' }
167
172
  expect(response.status).to eq(500)
173
+ expect(JSON.parse(response.body)).to eq({'error' => 'Error in smart action change hook: fields params is mandatory'})
168
174
  end
169
175
 
170
176
  it 'should respond 500 with bad hook result type' do
171
177
  post '/forest/actions/fail_action/hooks/change', params: JSON.dump(params), headers: { 'CONTENT_TYPE' => 'application/json' }
172
178
  expect(response.status).to eq(500)
173
- end
174
-
175
- it 'should respond 500 with bad hook result data structure' do
176
- post '/forest/actions/cheat_action/hooks/change', params: JSON.dump(params), headers: { 'CONTENT_TYPE' => 'application/json' }
177
- expect(response.status).to eq(500)
179
+ expect(JSON.parse(response.body)).to eq({'error' => 'Error in smart action load hook: hook must return an array of fields'})
178
180
  end
179
181
 
180
182
  it 'should reset value when enums has changed' do
@@ -0,0 +1,70 @@
1
+ module ForestLiana
2
+ describe SmartActionFieldValidator do
3
+ describe "self.validate_field" do
4
+ it "should raise an SmartActionInvalidFieldError with nil field" do
5
+ expect { SmartActionFieldValidator.validate_field(nil, "actionName") }.to raise_error(ForestLiana::Errors::SmartActionInvalidFieldError, 'Error while parsing action "actionName": The field attribute must be defined')
6
+ end
7
+
8
+ it "should raise an SmartActionInvalidFieldError with a field that is not a string" do
9
+ expect { SmartActionFieldValidator.validate_field({
10
+ :field => 5
11
+ }, "actionName") }.to raise_error(ForestLiana::Errors::SmartActionInvalidFieldError, 'Error while parsing action "actionName": The field attribute must be a string.')
12
+ end
13
+
14
+ it "should raise an SmartActionInvalidFieldError with description that is not a string" do
15
+ expect { SmartActionFieldValidator.validate_field({
16
+ :field => "field",
17
+ :description => 5
18
+ }, "actionName") }.to raise_error(ForestLiana::Errors::SmartActionInvalidFieldError, 'Error while parsing action "actionName" on field "field": The description attribute must be a string.')
19
+ end
20
+
21
+ it "should raise an SmartActionInvalidFieldError with an enums that is not an array" do
22
+ expect { SmartActionFieldValidator.validate_field({
23
+ :field => "field",
24
+ :enums => "NotAnArray"
25
+ }, "actionName") }.to raise_error(ForestLiana::Errors::SmartActionInvalidFieldError, 'Error while parsing action "actionName" on field "field": The enums attribute must be an array.')
26
+ end
27
+
28
+ it "should raise an SmartActionInvalidFieldError with a reference that is not a string" do
29
+ expect { SmartActionFieldValidator.validate_field({
30
+ :field => "field",
31
+ :type => "String",
32
+ :reference => 5
33
+ }, "actionName") }.to raise_error(ForestLiana::Errors::SmartActionInvalidFieldError, 'Error while parsing action "actionName" on field "field": The reference attribute must be a string.')
34
+ end
35
+
36
+ it "should raise an SmartActionInvalidFieldError with an invalid type" do
37
+ expect { SmartActionFieldValidator.validate_field({
38
+ :field => "field",
39
+ :type => "AbsolutelyNotAValidType"
40
+ }, "actionName") }.to raise_error(ForestLiana::Errors::SmartActionInvalidFieldError, 'Error while parsing action "actionName" on field "field": The type attribute must be a valid type. See the documentation for more information. https://docs.forestadmin.com/documentation/reference-guide/fields/create-and-manage-smart-fields#available-field-options.')
41
+ end
42
+
43
+ it "should not raise any error when everything is configured correctly" do
44
+ expect { SmartActionFieldValidator.validate_field({
45
+ :field => "field",
46
+ :type => "String",
47
+ :description => "field description"
48
+ }, "actionName") }.not_to raise_error
49
+ end
50
+ end
51
+
52
+ describe "self.validate_field_change_hook" do
53
+ it "should raise an SmartActionInvalidFieldHookError with an invalid type" do
54
+ expect { SmartActionFieldValidator.validate_field_change_hook({
55
+ :field => "field",
56
+ :type => "AbsolutelyNotAValidType",
57
+ :hook => "hookThatDoesNotExist"
58
+ }, "actionName", []) }.to raise_error(ForestLiana::Errors::SmartActionInvalidFieldHookError)
59
+ end
60
+
61
+ it "should not raise any error when everything is configured correctly" do
62
+ expect { SmartActionFieldValidator.validate_field_change_hook({
63
+ :field => "field",
64
+ :type => "AbsolutelyNotAValidType",
65
+ :hook => "on_field_changed"
66
+ }, "actionName", ["on_field_changed"]) }.not_to raise_error
67
+ end
68
+ end
69
+ end
70
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: forest_liana
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.3.8
4
+ version: 7.0.0.beta.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sandro Munda
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-26 00:00:00.000000000 Z
11
+ date: 2021-06-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -25,7 +25,7 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '4.0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: jsonapi-serializers
28
+ name: forestadmin-jsonapi-serializers
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
@@ -225,7 +225,6 @@ files:
225
225
  - app/helpers/forest_liana/adapter_helper.rb
226
226
  - app/helpers/forest_liana/application_helper.rb
227
227
  - app/helpers/forest_liana/decoration_helper.rb
228
- - app/helpers/forest_liana/is_same_data_structure_helper.rb
229
228
  - app/helpers/forest_liana/query_helper.rb
230
229
  - app/helpers/forest_liana/schema_helper.rb
231
230
  - app/helpers/forest_liana/widgets_helper.rb
@@ -283,6 +282,7 @@ files:
283
282
  - app/services/forest_liana/schema_utils.rb
284
283
  - app/services/forest_liana/scope_validator.rb
285
284
  - app/services/forest_liana/search_query_builder.rb
285
+ - app/services/forest_liana/smart_action_field_validator.rb
286
286
  - app/services/forest_liana/stat_getter.rb
287
287
  - app/services/forest_liana/stripe_base_getter.rb
288
288
  - app/services/forest_liana/stripe_invoice_getter.rb
@@ -316,6 +316,7 @@ files:
316
316
  - lib/generators/forest_liana/install_generator.rb
317
317
  - lib/tasks/display_apimap.rake
318
318
  - lib/tasks/send_apimap.rake
319
+ - spec/config/initializers/logger_spec.rb
319
320
  - spec/dummy/README.rdoc
320
321
  - spec/dummy/Rakefile
321
322
  - spec/dummy/app/assets/config/manifest.js
@@ -367,9 +368,9 @@ files:
367
368
  - spec/dummy/db/schema.rb
368
369
  - spec/dummy/lib/forest_liana/collections/location.rb
369
370
  - spec/dummy/lib/forest_liana/collections/user.rb
370
- - spec/helpers/forest_liana/is_same_data_structure_helper_spec.rb
371
371
  - spec/helpers/forest_liana/query_helper_spec.rb
372
372
  - spec/helpers/forest_liana/schema_helper_spec.rb
373
+ - spec/lib/forest_liana/schema_file_updater_spec.rb
373
374
  - spec/rails_helper.rb
374
375
  - spec/requests/actions_controller_spec.rb
375
376
  - spec/requests/authentications_spec.rb
@@ -386,6 +387,7 @@ files:
386
387
  - spec/services/forest_liana/permissions_getter_spec.rb
387
388
  - spec/services/forest_liana/resources_getter_spec.rb
388
389
  - spec/services/forest_liana/schema_adapter_spec.rb
390
+ - spec/services/forest_liana/smart_action_field_validator_spec.rb
389
391
  - spec/spec_helper.rb
390
392
  - test/dummy/README.rdoc
391
393
  - test/dummy/Rakefile
@@ -494,9 +496,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
494
496
  version: '0'
495
497
  required_rubygems_version: !ruby/object:Gem::Requirement
496
498
  requirements:
497
- - - ">="
499
+ - - ">"
498
500
  - !ruby/object:Gem::Version
499
- version: '0'
501
+ version: 1.3.1
500
502
  requirements: []
501
503
  rubygems_version: 3.1.2
502
504
  signing_key:
@@ -602,12 +604,13 @@ test_files:
602
604
  - spec/services/forest_liana/permissions_checker_acl_enabled_spec.rb
603
605
  - spec/services/forest_liana/filters_parser_spec.rb
604
606
  - spec/services/forest_liana/schema_adapter_spec.rb
607
+ - spec/services/forest_liana/smart_action_field_validator_spec.rb
605
608
  - spec/services/forest_liana/line_stat_getter_spec.rb
606
609
  - spec/services/forest_liana/permissions_formatter_spec.rb
607
610
  - spec/services/forest_liana/permissions_checker_live_queries_spec.rb
608
611
  - spec/services/forest_liana/apimap_sorter_spec.rb
609
612
  - spec/services/forest_liana/permissions_checker_acl_disabled_spec.rb
610
- - spec/helpers/forest_liana/is_same_data_structure_helper_spec.rb
613
+ - spec/config/initializers/logger_spec.rb
611
614
  - spec/helpers/forest_liana/schema_helper_spec.rb
612
615
  - spec/helpers/forest_liana/query_helper_spec.rb
613
616
  - spec/dummy/db/schema.rb
@@ -661,6 +664,7 @@ test_files:
661
664
  - spec/dummy/app/models/location.rb
662
665
  - spec/dummy/app/models/island.rb
663
666
  - spec/dummy/app/models/reference.rb
667
+ - spec/lib/forest_liana/schema_file_updater_spec.rb
664
668
  - spec/rails_helper.rb
665
669
  - spec/requests/stats_spec.rb
666
670
  - spec/requests/resources_spec.rb
@@ -1,44 +0,0 @@
1
- require 'set'
2
-
3
- module ForestLiana
4
- module IsSameDataStructureHelper
5
- class Analyser
6
- def initialize(object, other, deep = 0)
7
- @object = object
8
- @other = other
9
- @deep = deep
10
- end
11
-
12
- def are_objects(object, other)
13
- object && other && object.is_a?(Hash) && other.is_a?(Hash)
14
- end
15
-
16
- def check_keys(object, other, step = 0)
17
- unless are_objects(object, other)
18
- return false
19
- end
20
-
21
- object_keys = object.keys
22
- other_keys = other.keys
23
-
24
- if object_keys.length != other_keys.length
25
- return false
26
- end
27
-
28
- object_keys_set = object_keys.to_set
29
- other_keys.each { |key|
30
- if !object_keys_set.member?(key) || (step + 1 <= @deep && !check_keys(object[key], other[key], step + 1))
31
- return false
32
- end
33
- }
34
-
35
- return true
36
- end
37
-
38
- def perform
39
- check_keys(@object, @other)
40
- end
41
- end
42
- end
43
- end
44
-
@@ -1,87 +0,0 @@
1
- module ForestLiana
2
- context 'IsSameDataStructure class' do
3
- it 'should: be valid with simple data' do
4
- object = {:a => 'a', :b => 'b'}
5
- other = {:a => 'a', :b => 'b'}
6
- result = IsSameDataStructureHelper::Analyser.new(object, other).perform
7
- expect(result).to be true
8
- end
9
-
10
- it 'should: be invalid with simple data' do
11
- object = {:a => 'a', :b => 'b'}
12
- other = {:a => 'a', :c => 'c'}
13
- result = IsSameDataStructureHelper::Analyser.new(object, other).perform
14
- expect(result).to be false
15
- end
16
-
17
- it 'should: be invalid with not same hash' do
18
- object = {:a => 'a', :b => 'b'}
19
- other = {:a => 'a', :b => 'b', :c => 'c'}
20
- result = IsSameDataStructureHelper::Analyser.new(object, other).perform
21
- expect(result).to be false
22
- end
23
-
24
- it 'should: be invalid with nil' do
25
- object = nil
26
- other = {:a => 'a', :b => 'b', :c => 'c'}
27
- result = IsSameDataStructureHelper::Analyser.new(object, other).perform
28
- expect(result).to be false
29
- end
30
-
31
- it 'should: be invalid with not hash' do
32
- object = nil
33
- other = {:a => 'a', :b => 'b', :c => 'c'}
34
- result = IsSameDataStructureHelper::Analyser.new(object, other).perform
35
- expect(result).to be false
36
- end
37
-
38
- it 'should: be invalid with integer' do
39
- object = 1
40
- other = {:a => 'a', :b => 'b', :c => 'c'}
41
- result = IsSameDataStructureHelper::Analyser.new(object, other).perform
42
- expect(result).to be false
43
- end
44
-
45
- it 'should: be invalid with string' do
46
- object = 'a'
47
- other = {:a => 'a', :b => 'b', :c => 'c'}
48
- result = IsSameDataStructureHelper::Analyser.new(object, other).perform
49
- expect(result).to be false
50
- end
51
-
52
- it 'should: be valid with depth 1' do
53
- object = {:a => {:c => 'c'}, :b => {:d => 'd'}}
54
- other = {:a => {:c => 'c'}, :b => {:d => 'd'}}
55
- result = IsSameDataStructureHelper::Analyser.new(object, other, 1).perform
56
- expect(result).to be true
57
- end
58
-
59
- it 'should: be invalid with depth 1' do
60
- object = {:a => {:c => 'c'}, :b => {:d => 'd'}}
61
- other = {:a => {:c => 'c'}, :b => {:e => 'e'}}
62
- result = IsSameDataStructureHelper::Analyser.new(object, other, 1).perform
63
- expect(result).to be false
64
- end
65
-
66
- it 'should: be invalid with depth 1 and nil' do
67
- object = {:a => {:c => 'c'}, :b => {:d => 'd'}}
68
- other = {:a => {:c => 'c'}, :b => nil}
69
- result = IsSameDataStructureHelper::Analyser.new(object, other, 1).perform
70
- expect(result).to be false
71
- end
72
-
73
- it 'should: be invalid with depth 1 and integer' do
74
- object = {:a => {:c => 'c'}, :b => {:d => 'd'}}
75
- other = {:a => {:c => 'c'}, :b => 1}
76
- result = IsSameDataStructureHelper::Analyser.new(object, other, 1).perform
77
- expect(result).to be false
78
- end
79
-
80
- it 'should: be invalid with depth 1 and string' do
81
- object = {:a => {:c => 'c'}, :b => {:d => 'd'}}
82
- other = {:a => {:c => 'c'}, :b => 'b'}
83
- result = IsSameDataStructureHelper::Analyser.new(object, other, 1).perform
84
- expect(result).to be false
85
- end
86
- end
87
- end