scimitar 1.8.0 → 1.8.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b879f0ede10fe831a162adc658e6f2642d8c058afb5349ea8b753e2a9cc6c292
4
- data.tar.gz: 04644e6cb710a444684746d707170802f38b31586a4207016eb482c05e5e063e
3
+ metadata.gz: f83c7d7e476706bfc909fa2e08761aa544333d9fd32f077b0c453949f06cb8e9
4
+ data.tar.gz: a9c847ec6f3ebd6d16cb8efae0093bc6fca33e5e05975532aeb17983eed79d0d
5
5
  SHA512:
6
- metadata.gz: 6bf7d2818647dddc9ef9047b59391486edaba33af74179a0481fd18ab6f4a1e9917e700fe58699e6754ee64105cb747f687ca4514b415c5c246d082f649e7d54
7
- data.tar.gz: e92aaaaf03874468606a4f673f13b81edc799c724a43df6813b8e6836e06c5e76ba8fe0230fdfbc6fd089b4fc467440bdd35b72a833cb9c52fda8abeeebf4fd1
6
+ metadata.gz: 7ab73ba381492e4db44d42db596c897fdeb4609d24e7d5bc4f2f02d86db358030f6c3f9817e8c5209cd57212e0eeb79eed13fd1dd2f0f816f96af28217542827
7
+ data.tar.gz: c1961bb8075943be194bfe129db14d96e60a016001f56b61b51c001e589a7e56c53899c0119b9a07bf1a0e9ef204bd66d6b7802694a795bf969294000c2837b1
@@ -129,7 +129,7 @@ module Scimitar
129
129
 
130
130
  if scim_attribute && scim_attribute.complexType
131
131
  if scim_attribute.multiValued
132
- self.send("#{attr_name}=", attr_value.map {|attr_for_each_item| complex_type_from_hash(scim_attribute, attr_for_each_item)})
132
+ self.send("#{attr_name}=", attr_value&.map {|attr_for_each_item| complex_type_from_hash(scim_attribute, attr_for_each_item)})
133
133
  else
134
134
  self.send("#{attr_name}=", complex_type_from_hash(scim_attribute, attr_value))
135
135
  end
@@ -137,18 +137,21 @@ module Scimitar
137
137
  end
138
138
  end
139
139
 
140
+ # Renders *in full* as JSON; typically used for write-based operations...
141
+ #
142
+ # record = self.storage_class().new
143
+ # record.from_scim!(scim_hash: scim_resource.as_json())
144
+ # self.save!(record)
145
+ #
146
+ # ...so all fields, even those marked "returned: false", are included.
147
+ # Use Scimitar::Resources::Mixin::to_scim to obtain a SCIM object with
148
+ # non-returnable fields omitted, rendering *that* as JSON via #to_json.
149
+ #
140
150
  def as_json(options = {})
141
151
  self.meta = Meta.new unless self.meta && self.meta.is_a?(Meta)
142
152
  self.meta.resourceType = self.class.resource_type_id
143
153
 
144
- non_returnable_attributes = self.class
145
- .schemas
146
- .flat_map(&:scim_attributes)
147
- .filter_map { |attribute| attribute.name if attribute.returned == 'never' }
148
-
149
- non_returnable_attributes << 'errors'
150
-
151
- original_hash = super(options).except(*non_returnable_attributes)
154
+ original_hash = super(options).except('errors')
152
155
  original_hash.merge!('schemas' => self.class.schemas.map(&:id))
153
156
 
154
157
  self.class.extended_schemas.each do |extension_schema|
@@ -335,7 +335,8 @@ module Scimitar
335
335
  @scim_queryable_attributes ||= self.class.scim_queryable_attributes()
336
336
  end
337
337
 
338
- # Render self as a SCIM object using ::scim_attributes_map.
338
+ # Render self as a SCIM object using ::scim_attributes_map. Fields that
339
+ # are marked as <tt>returned: 'never'</tt> are excluded.
339
340
  #
340
341
  # +location+:: The location (HTTP(S) full URI) of this resource, in the
341
342
  # domain of the object including this mixin - "your" IDs,
@@ -344,9 +345,10 @@ module Scimitar
344
345
  #
345
346
  def to_scim(location:)
346
347
  map = self.class.scim_attributes_map()
348
+ resource_type = self.class.scim_resource_type()
347
349
  timestamps_map = self.class.scim_timestamps_map() if self.class.respond_to?(:scim_timestamps_map)
348
- attrs_hash = self.to_scim_backend(data_source: self, attrs_map_or_leaf_value: map)
349
- resource = self.class.scim_resource_type().new(attrs_hash)
350
+ attrs_hash = self.to_scim_backend(data_source: self, resource_type: resource_type, attrs_map_or_leaf_value: map)
351
+ resource = resource_type.new(attrs_hash)
350
352
  meta_attrs_hash = { location: location }
351
353
 
352
354
  meta_attrs_hash[:created ] = self.send(timestamps_map[:created ])&.iso8601(0) if timestamps_map&.key?(:created)
@@ -373,16 +375,39 @@ module Scimitar
373
375
  #
374
376
  # Call ONLY for POST or PUT. For PATCH, see #from_scim_patch!.
375
377
  #
376
- # +scim_hash+:: A Hash that's the result of parsing a JSON payload
377
- # from an inbound POST or PUT request.
378
+ # Mandatory named parameters:
379
+ #
380
+ # +scim_hash+:: A Hash that's the result of parsing a JSON payload
381
+ # from an inbound POST or PUT request.
382
+ #
383
+ # Optional named parameters:
384
+ #
385
+ # +with_clearing+:: According to RFC 7644 section 3.5.1, PUT operations
386
+ # MAY default or clear any attribute missing from
387
+ # +scim_hash+ as this is deemed "not asserted by the
388
+ # client" (see
389
+ # https://tools.ietf.org/html/rfc7644#section-3.5.1).
390
+ # This parameter controls such behaviour. It defaults
391
+ # to +true+, so clearing is applied - single value
392
+ # attributes are set to +nil+ and arrays are emptied.
393
+ # If +false+, an unusual <b>preservation</b> mode is
394
+ # applied and anything absent from +scim_hash+ will
395
+ # have no impact on the target object (any mapped
396
+ # attributes in the local data model with existing
397
+ # non-nil values will retain those values).
378
398
  #
379
399
  # Returns 'self', for convenience of e.g. chaining other methods.
380
400
  #
381
- def from_scim!(scim_hash:)
401
+ def from_scim!(scim_hash:, with_clearing: true)
382
402
  scim_hash.freeze()
383
403
  map = self.class.scim_attributes_map().freeze()
384
404
 
385
- self.from_scim_backend!(attrs_map_or_leaf_value: map, scim_hash_or_leaf_value: scim_hash)
405
+ self.from_scim_backend!(
406
+ attrs_map_or_leaf_value: map,
407
+ scim_hash_or_leaf_value: scim_hash,
408
+ with_clearing: with_clearing
409
+ )
410
+
386
411
  return self
387
412
  end
388
413
 
@@ -483,7 +508,8 @@ module Scimitar
483
508
  nature: nature,
484
509
  path: paths,
485
510
  value: value,
486
- altering_hash: ci_scim_hash
511
+ altering_hash: ci_scim_hash,
512
+ with_attr_map: self.class.scim_attributes_map()
487
513
  )
488
514
 
489
515
  if extract_root
@@ -491,7 +517,7 @@ module Scimitar
491
517
  end
492
518
  end
493
519
 
494
- self.from_scim!(scim_hash: ci_scim_hash)
520
+ self.from_scim!(scim_hash: ci_scim_hash, with_clearing: false)
495
521
  return self
496
522
  end
497
523
 
@@ -509,14 +535,48 @@ module Scimitar
509
535
  # this is "self" (an instance of the
510
536
  # class mixing in this module).
511
537
  #
538
+ # +resource_type+:: The resource type carrying the schemas
539
+ # describing the SCIM object. If at the
540
+ # top level when +data_source+ is +self+,
541
+ # this would be sent as
542
+ # <tt>self.class.scim_resource_type()</tt>.
543
+ #
512
544
  # +attrs_map_or_leaf_value+:: The attribute map. At the top level,
513
545
  # this is from ::scim_attributes_map.
514
546
  #
515
- def to_scim_backend(data_source:, attrs_map_or_leaf_value:)
547
+ # Internal recursive calls also send:
548
+ #
549
+ # +attribute_path+:: Array of path components to the
550
+ # attribute, which can be found through
551
+ # +resource_type+ so that things like the
552
+ # "+returned+" state can be checked.
553
+ #
554
+ def to_scim_backend(
555
+ data_source:,
556
+ resource_type:,
557
+ attrs_map_or_leaf_value:,
558
+ attribute_path: []
559
+ )
560
+
561
+ # On assumption of a top-level attributes list, the 'return never'
562
+ # state is only checked on the recursive call from a Hash type. The
563
+ # other handled types are assumed to only happen when called
564
+ # recursively, so no need to check as no such call is made for a
565
+ # 'return never' attribute.
566
+ #
516
567
  case attrs_map_or_leaf_value
517
568
  when Hash # Expected at top-level of any map, or nested within
518
569
  attrs_map_or_leaf_value.each.with_object({}) do |(key, value), hash|
519
- hash[key] = to_scim_backend(data_source: data_source, attrs_map_or_leaf_value: value)
570
+ nested_attribute_path = attribute_path + [key]
571
+
572
+ if resource_type.find_attribute(*nested_attribute_path)&.returned != "never"
573
+ hash[key] = to_scim_backend(
574
+ data_source: data_source,
575
+ resource_type: resource_type,
576
+ attribute_path: nested_attribute_path,
577
+ attrs_map_or_leaf_value: value
578
+ )
579
+ end
520
580
  end
521
581
 
522
582
  when Array # Static or dynamic mapping against lists in data source
@@ -527,14 +587,26 @@ module Scimitar
527
587
 
528
588
  elsif value.key?(:match) # Static map
529
589
  static_hash = { value[:match] => value[:with] }
530
- static_hash.merge!(to_scim_backend(data_source: data_source, attrs_map_or_leaf_value: value[:using]))
590
+ static_hash.merge!(
591
+ to_scim_backend(
592
+ data_source: data_source,
593
+ resource_type: resource_type,
594
+ attribute_path: attribute_path,
595
+ attrs_map_or_leaf_value: value[:using]
596
+ )
597
+ )
531
598
  static_hash
532
599
 
533
600
  elsif value.key?(:list) # Dynamic mapping of each complex list item
534
601
  built_dynamic_list = true
535
602
  list = data_source.public_send(value[:list])
536
603
  list.map do |list_entry|
537
- to_scim_backend(data_source: list_entry, attrs_map_or_leaf_value: value[:using])
604
+ to_scim_backend(
605
+ data_source: list_entry,
606
+ resource_type: resource_type,
607
+ attribute_path: attribute_path,
608
+ attrs_map_or_leaf_value: value[:using]
609
+ )
538
610
  end
539
611
 
540
612
  else # Unknown type, just treat as flat values
@@ -625,6 +697,15 @@ module Scimitar
625
697
  # read as input source material (left
626
698
  # hand side of the ASCII art diagram).
627
699
  #
700
+ # +with_clearing+:: If +true+, attributes absent in
701
+ # +scim_hash_or_leaf_value+ but present
702
+ # in +attrs_map_or_leaf_value+ will be
703
+ # cleared (+nil+ or empty array), for PUT
704
+ # ("replace") semantics. If +false+, such
705
+ # missing attribute values are left
706
+ # untouched - whatever mapped value is in
707
+ # +self+ is preserved.
708
+ #
628
709
  # +path+:: Array of SCIM attribute names giving a
629
710
  # path into the SCIM schema where
630
711
  # iteration has reached. Used to find the
@@ -634,6 +715,7 @@ module Scimitar
634
715
  def from_scim_backend!(
635
716
  attrs_map_or_leaf_value:,
636
717
  scim_hash_or_leaf_value:,
718
+ with_clearing:,
637
719
  path: []
638
720
  )
639
721
  scim_hash_or_leaf_value = scim_hash_or_leaf_value.with_indifferent_case_insensitive_access() if scim_hash_or_leaf_value.is_a?(Hash)
@@ -664,13 +746,29 @@ module Scimitar
664
746
  end
665
747
  attribute_tree << scim_attribute.to_s
666
748
 
667
- sub_scim_hash_or_leaf_value = scim_hash_or_leaf_value&.dig(*attribute_tree)
749
+ continue_processing = if with_clearing
750
+ true
751
+ else
752
+ most_of_attribute_tree = attribute_tree[...-1]
753
+ last_attribute_in_tree = attribute_tree.last
754
+
755
+ if most_of_attribute_tree.empty?
756
+ scim_hash_or_leaf_value&.key?(last_attribute_in_tree)
757
+ else
758
+ scim_hash_or_leaf_value&.dig(*most_of_attribute_tree)&.key?(last_attribute_in_tree)
759
+ end
760
+ end
761
+
762
+ if continue_processing
763
+ sub_scim_hash_or_leaf_value = scim_hash_or_leaf_value&.dig(*attribute_tree)
668
764
 
669
- self.from_scim_backend!(
670
- attrs_map_or_leaf_value: sub_attrs_map_or_leaf_value,
671
- scim_hash_or_leaf_value: sub_scim_hash_or_leaf_value, # May be 'nil'
672
- path: path + [scim_attribute]
673
- )
765
+ self.from_scim_backend!(
766
+ attrs_map_or_leaf_value: sub_attrs_map_or_leaf_value,
767
+ scim_hash_or_leaf_value: sub_scim_hash_or_leaf_value, # May be 'nil'
768
+ with_clearing: with_clearing,
769
+ path: path + [scim_attribute]
770
+ )
771
+ end
674
772
  end
675
773
 
676
774
  when Array # Static or dynamic maps
@@ -692,6 +790,7 @@ module Scimitar
692
790
  self.from_scim_backend!(
693
791
  attrs_map_or_leaf_value: sub_attrs_map,
694
792
  scim_hash_or_leaf_value: found_source_list_entry, # May be 'nil'
793
+ with_clearing: with_clearing,
695
794
  path: path
696
795
  )
697
796
 
@@ -747,7 +846,9 @@ module Scimitar
747
846
  # +path+:: Operation path, as a series of array entries (so
748
847
  # an inbound dot-separated path string would first
749
848
  # be split into an array by the caller). For
750
- # internal recursive calls, this will
849
+ # internal recursive calls, this will be a subset
850
+ # of array entries from an index somewhere into the
851
+ # top-level array, through to its end.
751
852
  #
752
853
  # +value+:: The value to apply at the attribute(s) identified
753
854
  # by +path+. Ignored for 'remove' operations.
@@ -763,7 +864,7 @@ module Scimitar
763
864
  # own wrapping Hash with a single key addressing the SCIM object of
764
865
  # interest and supply this key as the sole array entry in +path+.
765
866
  #
766
- def from_patch_backend!(nature:, path:, value:, altering_hash:)
867
+ def from_patch_backend!(nature:, path:, value:, altering_hash:, with_attr_map:)
767
868
  raise 'Case sensitivity violation' unless altering_hash.is_a?(Scimitar::Support::HashWithIndifferentCaseInsensitiveAccess)
768
869
 
769
870
  # These all throw exceptions if data is not as expected / required,
@@ -774,14 +875,16 @@ module Scimitar
774
875
  nature: nature,
775
876
  path: path,
776
877
  value: value,
777
- altering_hash: altering_hash
878
+ altering_hash: altering_hash,
879
+ with_attr_map: with_attr_map
778
880
  )
779
881
  else
780
882
  from_patch_backend_traverse!(
781
883
  nature: nature,
782
884
  path: path,
783
885
  value: value,
784
- altering_hash: altering_hash
886
+ altering_hash: altering_hash,
887
+ with_attr_map: with_attr_map
785
888
  )
786
889
  end
787
890
 
@@ -801,7 +904,7 @@ module Scimitar
801
904
  #
802
905
  # Happily throws exceptions if data is not as expected / required.
803
906
  #
804
- def from_patch_backend_traverse!(nature:, path:, value:, altering_hash:)
907
+ def from_patch_backend_traverse!(nature:, path:, value:, altering_hash:, with_attr_map:)
805
908
  raise 'Case sensitivity violation' unless altering_hash.is_a?(Scimitar::Support::HashWithIndifferentCaseInsensitiveAccess)
806
909
 
807
910
  path_component, filter = extract_filter_from(path_component: path.first)
@@ -847,11 +950,23 @@ module Scimitar
847
950
  end
848
951
 
849
952
  found_data_for_recursion.each do | found_data |
953
+ attr_map = with_attr_map[path_component.to_sym]
954
+
955
+ # Static array mappings need us to find the right map entry that
956
+ # corresponds to the SCIM data at hand and recurse back into the
957
+ # patch engine with the ":using" attribute map data.
958
+ #
959
+ if attr_map.is_a?(Array)
960
+ array_attr_map = find_matching_static_attr_map(data: found_data, with_attr_map: attr_map)
961
+ attr_map = array_attr_map unless array_attr_map.nil?
962
+ end
963
+
850
964
  self.from_patch_backend!(
851
965
  nature: nature,
852
- path: path[1..-1],
966
+ path: path[1..],
853
967
  value: value,
854
- altering_hash: found_data
968
+ altering_hash: found_data,
969
+ with_attr_map: attr_map
855
970
  )
856
971
  end
857
972
  end
@@ -866,7 +981,7 @@ module Scimitar
866
981
  #
867
982
  # Happily throws exceptions if data is not as expected / required.
868
983
  #
869
- def from_patch_backend_apply!(nature:, path:, value:, altering_hash:)
984
+ def from_patch_backend_apply!(nature:, path:, value:, altering_hash:, with_attr_map:)
870
985
  raise 'Case sensitivity violation' unless altering_hash.is_a?(Scimitar::Support::HashWithIndifferentCaseInsensitiveAccess)
871
986
 
872
987
  path_component, filter = extract_filter_from(path_component: path.first)
@@ -896,11 +1011,48 @@ module Scimitar
896
1011
 
897
1012
  case nature
898
1013
  when 'remove'
899
- current_data_at_path[matched_index] = nil
900
- compact_after = true
1014
+ handled = false
1015
+ attr_map_path = path[..-2] + [path_component]
1016
+ attr_map_entry = with_attr_map.dig(*attr_map_path.map(&:to_sym))
1017
+
1018
+ # Deal with arrays specially; static maps require specific
1019
+ # treatment, but dynamic or actual array values do not.
1020
+ #
1021
+ if attr_map_entry.is_a?(Array)
1022
+ array_attr_map = find_matching_static_attr_map(
1023
+ data: matched_hash,
1024
+ with_attr_map: attr_map_entry
1025
+ )
1026
+
1027
+ # Found? Run through the mapped attributes. Anything that
1028
+ # has an associated model attribute (i.e. some property
1029
+ # that must be to be written into local data in response
1030
+ # to the SCIM attribute being changed) is 'removed' by
1031
+ # setting the corresponding value in "altering_hash" (of
1032
+ # which "matched_hash" referenced fragment) to "nil".
1033
+ #
1034
+ handled = clear_data_for_removal!(
1035
+ altering_hash: matched_hash,
1036
+ with_attr_map: array_attr_map
1037
+ )
1038
+ end
1039
+
1040
+ # For dynamic arrays or other value types, we assume that
1041
+ # just clearing the item from the array or setting its SCIM
1042
+ # attribute to "nil" will result in an appropriate update
1043
+ # to the local data model (e.g. by a change in an Rails
1044
+ # associated collection or clearing a local model attribute
1045
+ # directly to "nil").
1046
+ #
1047
+ if handled == false
1048
+ current_data_at_path[matched_index] = nil
1049
+ compact_after = true
1050
+ end
1051
+
901
1052
  when 'replace'
902
1053
  matched_hash.reject! { true }
903
1054
  matched_hash.merge!(value)
1055
+
904
1056
  end
905
1057
  end
906
1058
 
@@ -943,7 +1095,8 @@ module Scimitar
943
1095
  nature: nature,
944
1096
  path: path + [key],
945
1097
  value: value[key],
946
- altering_hash: altering_hash
1098
+ altering_hash: altering_hash,
1099
+ with_attr_map: with_attr_map
947
1100
  )
948
1101
  end
949
1102
  else
@@ -955,6 +1108,7 @@ module Scimitar
955
1108
  dot_pathed_value = value.inject({}) do |hash, (k, v)|
956
1109
  hash.deep_merge!(::Scimitar::Support::Utilities.dot_path(k.split('.'), v))
957
1110
  end
1111
+
958
1112
  altering_hash[path_component].deep_merge!(dot_pathed_value)
959
1113
  else
960
1114
  altering_hash[path_component] = value
@@ -1013,8 +1167,8 @@ module Scimitar
1013
1167
  # integer primary keys, which all end up as strings anyway.
1014
1168
  #
1015
1169
  value.each do | value_item |
1016
- altering_hash[path_component].delete_if do | item |
1017
- if item.is_a?(Hash) && value_item.is_a?(Hash)
1170
+ altering_hash[path_component].map! do | item |
1171
+ item_is_matched = if item.is_a?(Hash) && value_item.is_a?(Hash)
1018
1172
  matched_all = true
1019
1173
  value_item.each do | value_key, value_value |
1020
1174
  next if value_key == '$ref'
@@ -1026,10 +1180,55 @@ module Scimitar
1026
1180
  else
1027
1181
  item&.to_s == value_item&.to_s
1028
1182
  end
1183
+
1184
+ if item_is_matched
1185
+ handled = false
1186
+ attr_map_path = path[..-2] + [path_component]
1187
+ attr_map_entry = with_attr_map.dig(*attr_map_path.map(&:to_sym))
1188
+ array_attr_map = find_matching_static_attr_map(
1189
+ data: item,
1190
+ with_attr_map: attr_map_entry
1191
+ )
1192
+
1193
+ handled = clear_data_for_removal!(
1194
+ altering_hash: item,
1195
+ with_attr_map: array_attr_map
1196
+ )
1197
+
1198
+ handled ? item : nil
1199
+ else
1200
+ item
1201
+ end
1202
+ end
1203
+
1204
+ altering_hash[path_component].compact!
1205
+ end
1206
+
1207
+ elsif altering_hash[path_component].is_a?(Array)
1208
+ handled = false
1209
+ attr_map_path = path[..-2] + [path_component]
1210
+ attr_map_entry = with_attr_map.dig(*attr_map_path.map(&:to_sym))
1211
+
1212
+ if attr_map_entry.is_a?(Array) # Array mapping
1213
+ altering_hash[path_component].each do | data_to_check |
1214
+ array_attr_map = find_matching_static_attr_map(
1215
+ data: data_to_check,
1216
+ with_attr_map: attr_map_entry
1217
+ )
1218
+
1219
+ handled = clear_data_for_removal!(
1220
+ altering_hash: data_to_check,
1221
+ with_attr_map: array_attr_map
1222
+ )
1029
1223
  end
1030
1224
  end
1225
+
1226
+ if handled == false
1227
+ altering_hash[path_component] = []
1228
+ end
1229
+
1031
1230
  else
1032
- altering_hash.delete(path_component)
1231
+ altering_hash[path_component] = nil
1033
1232
  end
1034
1233
 
1035
1234
  end
@@ -1108,6 +1307,177 @@ module Scimitar
1108
1307
  end
1109
1308
  end
1110
1309
 
1310
+ # Static attribute maps are used where SCIM attributes include some
1311
+ # kind of array, but it's not an arbitrary collection (dynamic maps
1312
+ # handle those). Instead, specific matched values inside the SCIM
1313
+ # data are mapped to specific attributes in the local data model.
1314
+ #
1315
+ # A typical example is for e-mails, where the SCIM "type" field in an
1316
+ # array of e-mail addresses might get mapped to detect specific types
1317
+ # of address such as "work" and "home", which happen to be stored
1318
+ # locally in dedicated attributes (e.g. "work_email_address").
1319
+ #
1320
+ # During certain processing operations we end up with a set of data
1321
+ # sent in from some SCIM operation and need to make modifications
1322
+ # (e.g. for a PATCH) that require the attribute map corresponding to
1323
+ # each part of the inbound SCIM data to be known. That's where this
1324
+ # method comes in. Usually, it's not hard to traverse a path of SCIM
1325
+ # data and dig a corresponding path through the attribute map Hash,
1326
+ # except for static arrays. There, we need to know which of the
1327
+ # static map entries matches a piece of SCIM data *from entries* in
1328
+ # the array of SCIM data corresponding to the static map.
1329
+ #
1330
+ # Call here with a piece of SCIM data from an array, along with an
1331
+ # attribute map fragment that must be the Array containing mappings.
1332
+ # Static mapping entries from this are compared with the data and if
1333
+ # a match is found, the sub-attribute map from the static entry's
1334
+ # <tt>:using</tt> key is returned; else +nil+.
1335
+ #
1336
+ # Named parameters are:
1337
+ #
1338
+ # +data+:: A SCIM data entry from a SCIM data array which is
1339
+ # mapped via the data given in the +with_attr_map+
1340
+ # parameter.
1341
+ #
1342
+ # +with_attr_map+:: The attributes map fragment which must be an
1343
+ # Array of mappings for the corresponding array
1344
+ # in the SCIM data from which +data+ was drawn.
1345
+ #
1346
+ # For example, if SCIM data consisted of:
1347
+ #
1348
+ # {
1349
+ # 'emails' => [
1350
+ # {
1351
+ # 'type' => 'work',
1352
+ # 'value' => 'work_1@test.com'
1353
+ # },
1354
+ # {
1355
+ # 'type' => 'work',
1356
+ # 'value' => 'work_2@test.com'
1357
+ # }
1358
+ # ]
1359
+ # }
1360
+ #
1361
+ # ...which was mapped to the local data model using the following
1362
+ # attribute map:
1363
+ #
1364
+ # {
1365
+ # emails: [
1366
+ # { match: 'type', with: 'home', using: { value: :home_email } },
1367
+ # { match: 'type', with: 'work', using: { value: :work_email } },
1368
+ # ]
1369
+ # }
1370
+ #
1371
+ # ...then when it came to processing the SCIM 'emails' entry, one of
1372
+ # the array _entries_ therein would be passed in +data+, while the
1373
+ # attribute map's <tt>:emails</tt> key's value (the _array_ of map
1374
+ # data) would be given in <tt>:with_attr_map</tt>. The first SCIM
1375
+ # array entry matches +work+ so the <tt>:using</tt> part of the map
1376
+ # for that match would be returned:
1377
+ #
1378
+ # { value: :work_email }
1379
+ #
1380
+ # If there was a SCIM entry with a type of something unrecognised,
1381
+ # such as 'holday', then +nil+ would be returned since there is no
1382
+ # matching attribute map entry.
1383
+ #
1384
+ # Note that the <tt>:with_attr_map</tt> array can contain dynamic
1385
+ # mappings or even be just a simple fixed array - only things that
1386
+ # "look like" static mapping entries are processed (i.e. Hashes with
1387
+ # a Symbol key of <tt>:match</tt> present), with the rest ignored.
1388
+ #
1389
+ def find_matching_static_attr_map(data:, with_attr_map:)
1390
+ matched_map = with_attr_map.find do | static_or_dynamic_mapping |
1391
+
1392
+ # Only interested in Static Array mappings.
1393
+ #
1394
+ if static_or_dynamic_mapping.is_a?(Hash) && static_or_dynamic_mapping.key?(:match)
1395
+
1396
+ attr_to_match = static_or_dynamic_mapping[:match].to_s
1397
+ value_to_match = static_or_dynamic_mapping[:with]
1398
+ sub_attrs_map = static_or_dynamic_mapping[:using]
1399
+
1400
+ # If this mapping refers to the matched data at hand,
1401
+ # then we can process it further (see later below.
1402
+ #
1403
+ found = data[attr_to_match] == value_to_match
1404
+
1405
+ # Not found? No static map match perhaps; this could be
1406
+ # because a filter worked on a value which is fixed in
1407
+ # the static map. For example, a filter might check for
1408
+ # emails with "primary true", and the emergence of the
1409
+ # value for "primary" might not be in the data model -
1410
+ # it could be a constant declared in the 'using' part
1411
+ # of a static map. Ugh! Check for that.
1412
+ #
1413
+ unless found
1414
+ sub_attrs_map.each do | scim_attr, model_attr_or_constant |
1415
+
1416
+ # Only want constants such as 'true' or 'false'.
1417
+ #
1418
+ next if model_attr_or_constant.is_a?(Symbol)
1419
+
1420
+ # Does the static value match in the source data?
1421
+ # E.g. a SCIM attribute :primary with value 'true'.
1422
+ #
1423
+ if data[scim_attr] == model_attr_or_constant
1424
+ found = true
1425
+ break
1426
+ end
1427
+ end
1428
+ end
1429
+
1430
+ found
1431
+ else
1432
+ false
1433
+ end
1434
+ end
1435
+
1436
+ return matched_map&.dig(:using)
1437
+ end
1438
+
1439
+ # Related to #find_matching_static_attr_map - often, the reason to
1440
+ # find a static array entry related to some inbound SCIM data is for
1441
+ # a removal operation, where the way to "remove" the data in the
1442
+ # local data model is to set an attribute to "nil". This means you
1443
+ # need to know if there is an attribute writer related to the SCIM
1444
+ # data being removed - and #find_matching_static_attr_map helps.
1445
+ #
1446
+ # With that done, you can call here with the hash data to be changed
1447
+ # and fragment of attribute map that #find_matching_static_attr_map
1448
+ # (or something like it) found.
1449
+ #
1450
+ # +altering_hash+:: The fragment of SCIM data that might be updated
1451
+ # with +nil+ to ultimately lead to an atttribute
1452
+ # writer identified through +with_attr_map+ being
1453
+ # called with that value. This is often the same
1454
+ # that was passed in the +data+ attribute in a
1455
+ # prior #find_matching_static_attr_map call.
1456
+ #
1457
+ # +with_attr_map:: The map fragment that corresponds exactly to the
1458
+ # +altering_hash+ data - e.g. the return value of a
1459
+ # prior #find_matching_static_attr_map call.
1460
+ #
1461
+ # Update +altering_hash+ in place if the map finds a relevant local
1462
+ # data model attribute and returns +true+. If no changes are made,
1463
+ # returns +false+.
1464
+ #
1465
+ def clear_data_for_removal!(altering_hash:, with_attr_map:)
1466
+ handled = false
1467
+
1468
+ with_attr_map&.each do | scim_attr, model_attr_or_constant |
1469
+
1470
+ # Only process attribute names, not constants.
1471
+ #
1472
+ next unless model_attr_or_constant.is_a?(Symbol)
1473
+
1474
+ altering_hash[scim_attr] = nil
1475
+ handled = true
1476
+ end
1477
+
1478
+ return handled
1479
+ end
1480
+
1111
1481
  end # "included do"
1112
1482
  end # "module Mixin"
1113
1483
  end # "module Resources"