scimitar 2.7.1 → 2.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
 
@@ -458,32 +483,21 @@ module Scimitar
458
483
  ci_scim_hash = { 'root' => ci_scim_hash }.with_indifferent_case_insensitive_access()
459
484
  end
460
485
 
461
- # Handle extension schema. Contributed by @bettysteger and
462
- # @MorrisFreeman via:
463
- #
464
- # https://github.com/RIPAGlobal/scimitar/issues/48
465
- # https://github.com/RIPAGlobal/scimitar/pull/49
466
- #
467
- # Note the ":" separating the schema ID (URN) from the attribute.
468
- # The nature of JSON rendering / other payloads might lead you to
469
- # expect a "." as with any complex types, but that's not the case;
470
- # see https://tools.ietf.org/html/rfc7644#section-3.10, or
471
- # https://tools.ietf.org/html/rfc7644#section-3.5.2 of which in
472
- # particular, https://tools.ietf.org/html/rfc7644#page-35.
486
+ # Split the path into an array of path components, in a way
487
+ # which is aware of extension schemas. See documentation of
488
+ # Scimitar::Support::Utilities.path_str_to_array for details.
473
489
  #
474
- paths = []
475
- self.class.scim_resource_type.extended_schemas.each do |schema|
476
- path_str.downcase.split(schema.id.downcase + ':').drop(1).each do |path|
477
- paths += [schema.id] + path.split('.')
478
- end
479
- end
480
- paths = path_str.split('.') if paths.empty?
490
+ paths = ::Scimitar::Support::Utilities.path_str_to_array(
491
+ self.class.scim_resource_type.extended_schemas,
492
+ path_str
493
+ )
481
494
 
482
495
  self.from_patch_backend!(
483
496
  nature: nature,
484
497
  path: paths,
485
498
  value: value,
486
- altering_hash: ci_scim_hash
499
+ altering_hash: ci_scim_hash,
500
+ with_attr_map: self.class.scim_attributes_map()
487
501
  )
488
502
 
489
503
  if extract_root
@@ -491,7 +505,7 @@ module Scimitar
491
505
  end
492
506
  end
493
507
 
494
- self.from_scim!(scim_hash: ci_scim_hash)
508
+ self.from_scim!(scim_hash: ci_scim_hash, with_clearing: false)
495
509
  return self
496
510
  end
497
511
 
@@ -509,14 +523,48 @@ module Scimitar
509
523
  # this is "self" (an instance of the
510
524
  # class mixing in this module).
511
525
  #
526
+ # +resource_type+:: The resource type carrying the schemas
527
+ # describing the SCIM object. If at the
528
+ # top level when +data_source+ is +self+,
529
+ # this would be sent as
530
+ # <tt>self.class.scim_resource_type()</tt>.
531
+ #
512
532
  # +attrs_map_or_leaf_value+:: The attribute map. At the top level,
513
533
  # this is from ::scim_attributes_map.
514
534
  #
515
- def to_scim_backend(data_source:, attrs_map_or_leaf_value:)
535
+ # Internal recursive calls also send:
536
+ #
537
+ # +attribute_path+:: Array of path components to the
538
+ # attribute, which can be found through
539
+ # +resource_type+ so that things like the
540
+ # "+returned+" state can be checked.
541
+ #
542
+ def to_scim_backend(
543
+ data_source:,
544
+ resource_type:,
545
+ attrs_map_or_leaf_value:,
546
+ attribute_path: []
547
+ )
548
+
549
+ # On assumption of a top-level attributes list, the 'return never'
550
+ # state is only checked on the recursive call from a Hash type. The
551
+ # other handled types are assumed to only happen when called
552
+ # recursively, so no need to check as no such call is made for a
553
+ # 'return never' attribute.
554
+ #
516
555
  case attrs_map_or_leaf_value
517
556
  when Hash # Expected at top-level of any map, or nested within
518
557
  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)
558
+ nested_attribute_path = attribute_path + [key]
559
+
560
+ if resource_type.find_attribute(*nested_attribute_path)&.returned != "never"
561
+ hash[key] = to_scim_backend(
562
+ data_source: data_source,
563
+ resource_type: resource_type,
564
+ attribute_path: nested_attribute_path,
565
+ attrs_map_or_leaf_value: value
566
+ )
567
+ end
520
568
  end
521
569
 
522
570
  when Array # Static or dynamic mapping against lists in data source
@@ -527,14 +575,26 @@ module Scimitar
527
575
 
528
576
  elsif value.key?(:match) # Static map
529
577
  static_hash = { value[:match] => value[:with] }
530
- static_hash.merge!(to_scim_backend(data_source: data_source, attrs_map_or_leaf_value: value[:using]))
578
+ static_hash.merge!(
579
+ to_scim_backend(
580
+ data_source: data_source,
581
+ resource_type: resource_type,
582
+ attribute_path: attribute_path,
583
+ attrs_map_or_leaf_value: value[:using]
584
+ )
585
+ )
531
586
  static_hash
532
587
 
533
588
  elsif value.key?(:list) # Dynamic mapping of each complex list item
534
589
  built_dynamic_list = true
535
590
  list = data_source.public_send(value[:list])
536
591
  list.map do |list_entry|
537
- to_scim_backend(data_source: list_entry, attrs_map_or_leaf_value: value[:using])
592
+ to_scim_backend(
593
+ data_source: list_entry,
594
+ resource_type: resource_type,
595
+ attribute_path: attribute_path,
596
+ attrs_map_or_leaf_value: value[:using]
597
+ )
538
598
  end
539
599
 
540
600
  else # Unknown type, just treat as flat values
@@ -625,6 +685,15 @@ module Scimitar
625
685
  # read as input source material (left
626
686
  # hand side of the ASCII art diagram).
627
687
  #
688
+ # +with_clearing+:: If +true+, attributes absent in
689
+ # +scim_hash_or_leaf_value+ but present
690
+ # in +attrs_map_or_leaf_value+ will be
691
+ # cleared (+nil+ or empty array), for PUT
692
+ # ("replace") semantics. If +false+, such
693
+ # missing attribute values are left
694
+ # untouched - whatever mapped value is in
695
+ # +self+ is preserved.
696
+ #
628
697
  # +path+:: Array of SCIM attribute names giving a
629
698
  # path into the SCIM schema where
630
699
  # iteration has reached. Used to find the
@@ -634,6 +703,7 @@ module Scimitar
634
703
  def from_scim_backend!(
635
704
  attrs_map_or_leaf_value:,
636
705
  scim_hash_or_leaf_value:,
706
+ with_clearing:,
637
707
  path: []
638
708
  )
639
709
  scim_hash_or_leaf_value = scim_hash_or_leaf_value.with_indifferent_case_insensitive_access() if scim_hash_or_leaf_value.is_a?(Hash)
@@ -658,19 +728,43 @@ module Scimitar
658
728
  # https://github.com/RIPAGlobal/scimitar/issues/48
659
729
  # https://github.com/RIPAGlobal/scimitar/pull/49
660
730
  #
731
+ # Note the shortcoming that attribute names within extensions
732
+ # must be unique, as this mechanism basically just pulls out
733
+ # extension attributes to the top level, losing what amounts
734
+ # to the namespace that the extension schema ID provides.
735
+ #
661
736
  attribute_tree = []
662
737
  resource_class.extended_schemas.each do |schema|
663
- attribute_tree << schema.id and break if schema.scim_attributes.any? { |attribute| attribute.name == scim_attribute.to_s }
738
+ if schema.scim_attributes.any? { |attribute| attribute.name == scim_attribute.to_s }
739
+ attribute_tree << schema.id
740
+ break # NOTE EARLY LOOP EXIT
741
+ end
664
742
  end
665
743
  attribute_tree << scim_attribute.to_s
666
744
 
667
- sub_scim_hash_or_leaf_value = scim_hash_or_leaf_value&.dig(*attribute_tree)
745
+ continue_processing = if with_clearing
746
+ true
747
+ else
748
+ most_of_attribute_tree = attribute_tree[...-1]
749
+ last_attribute_in_tree = attribute_tree.last
750
+
751
+ if most_of_attribute_tree.empty?
752
+ scim_hash_or_leaf_value&.key?(last_attribute_in_tree)
753
+ else
754
+ scim_hash_or_leaf_value&.dig(*most_of_attribute_tree)&.key?(last_attribute_in_tree)
755
+ end
756
+ end
757
+
758
+ if continue_processing
759
+ sub_scim_hash_or_leaf_value = scim_hash_or_leaf_value&.dig(*attribute_tree)
668
760
 
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
- )
761
+ self.from_scim_backend!(
762
+ attrs_map_or_leaf_value: sub_attrs_map_or_leaf_value,
763
+ scim_hash_or_leaf_value: sub_scim_hash_or_leaf_value, # May be 'nil'
764
+ with_clearing: with_clearing,
765
+ path: path + [scim_attribute]
766
+ )
767
+ end
674
768
  end
675
769
 
676
770
  when Array # Static or dynamic maps
@@ -692,6 +786,7 @@ module Scimitar
692
786
  self.from_scim_backend!(
693
787
  attrs_map_or_leaf_value: sub_attrs_map,
694
788
  scim_hash_or_leaf_value: found_source_list_entry, # May be 'nil'
789
+ with_clearing: with_clearing,
695
790
  path: path
696
791
  )
697
792
 
@@ -747,7 +842,9 @@ module Scimitar
747
842
  # +path+:: Operation path, as a series of array entries (so
748
843
  # an inbound dot-separated path string would first
749
844
  # be split into an array by the caller). For
750
- # internal recursive calls, this will
845
+ # internal recursive calls, this will be a subset
846
+ # of array entries from an index somewhere into the
847
+ # top-level array, through to its end.
751
848
  #
752
849
  # +value+:: The value to apply at the attribute(s) identified
753
850
  # by +path+. Ignored for 'remove' operations.
@@ -763,7 +860,7 @@ module Scimitar
763
860
  # own wrapping Hash with a single key addressing the SCIM object of
764
861
  # interest and supply this key as the sole array entry in +path+.
765
862
  #
766
- def from_patch_backend!(nature:, path:, value:, altering_hash:)
863
+ def from_patch_backend!(nature:, path:, value:, altering_hash:, with_attr_map:)
767
864
  raise 'Case sensitivity violation' unless altering_hash.is_a?(Scimitar::Support::HashWithIndifferentCaseInsensitiveAccess)
768
865
 
769
866
  # These all throw exceptions if data is not as expected / required,
@@ -774,14 +871,16 @@ module Scimitar
774
871
  nature: nature,
775
872
  path: path,
776
873
  value: value,
777
- altering_hash: altering_hash
874
+ altering_hash: altering_hash,
875
+ with_attr_map: with_attr_map
778
876
  )
779
877
  else
780
878
  from_patch_backend_traverse!(
781
879
  nature: nature,
782
880
  path: path,
783
881
  value: value,
784
- altering_hash: altering_hash
882
+ altering_hash: altering_hash,
883
+ with_attr_map: with_attr_map
785
884
  )
786
885
  end
787
886
 
@@ -801,7 +900,7 @@ module Scimitar
801
900
  #
802
901
  # Happily throws exceptions if data is not as expected / required.
803
902
  #
804
- def from_patch_backend_traverse!(nature:, path:, value:, altering_hash:)
903
+ def from_patch_backend_traverse!(nature:, path:, value:, altering_hash:, with_attr_map:)
805
904
  raise 'Case sensitivity violation' unless altering_hash.is_a?(Scimitar::Support::HashWithIndifferentCaseInsensitiveAccess)
806
905
 
807
906
  path_component, filter = extract_filter_from(path_component: path.first)
@@ -847,11 +946,27 @@ module Scimitar
847
946
  end
848
947
 
849
948
  found_data_for_recursion.each do | found_data |
949
+ attr_map = if path_component.to_sym == :root
950
+ with_attr_map
951
+ else
952
+ with_attr_map[path_component.to_sym]
953
+ end
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
 
@@ -939,11 +1091,30 @@ module Scimitar
939
1091
  # at key 'members' with the above, rather than adding.
940
1092
  #
941
1093
  value.keys.each do | key |
1094
+
1095
+ # Handle the Azure (Entra) case where keys might use
1096
+ # dotted paths - see:
1097
+ #
1098
+ # https://github.com/RIPAGlobal/scimitar/issues/123
1099
+ #
1100
+ # ...along with keys containing schema IDs - see:
1101
+ #
1102
+ # https://is.docs.wso2.com/en/next/apis/scim2-patch-operations/#add-user-attributes
1103
+ #
1104
+ # ...and scroll down to example 3 of "Complex singular
1105
+ # attributes".
1106
+ #
1107
+ subpaths = ::Scimitar::Support::Utilities.path_str_to_array(
1108
+ self.class.scim_resource_type.extended_schemas,
1109
+ key
1110
+ )
1111
+
942
1112
  from_patch_backend!(
943
1113
  nature: nature,
944
- path: path + [key],
1114
+ path: path + subpaths,
945
1115
  value: value[key],
946
- altering_hash: altering_hash
1116
+ altering_hash: altering_hash,
1117
+ with_attr_map: with_attr_map
947
1118
  )
948
1119
  end
949
1120
  else
@@ -953,8 +1124,14 @@ module Scimitar
953
1124
  when 'replace'
954
1125
  if path_component == 'root'
955
1126
  dot_pathed_value = value.inject({}) do |hash, (k, v)|
956
- hash.deep_merge!(::Scimitar::Support::Utilities.dot_path(k.split('.'), v))
1127
+ subpaths = ::Scimitar::Support::Utilities.path_str_to_array(
1128
+ self.class.scim_resource_type.extended_schemas,
1129
+ k
1130
+ )
1131
+
1132
+ hash.deep_merge!(::Scimitar::Support::Utilities.dot_path(subpaths, v))
957
1133
  end
1134
+
958
1135
  altering_hash[path_component].deep_merge!(dot_pathed_value)
959
1136
  else
960
1137
  altering_hash[path_component] = value
@@ -1013,8 +1190,8 @@ module Scimitar
1013
1190
  # integer primary keys, which all end up as strings anyway.
1014
1191
  #
1015
1192
  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)
1193
+ altering_hash[path_component].map! do | item |
1194
+ item_is_matched = if item.is_a?(Hash) && value_item.is_a?(Hash)
1018
1195
  matched_all = true
1019
1196
  value_item.each do | value_key, value_value |
1020
1197
  next if value_key == '$ref'
@@ -1026,10 +1203,55 @@ module Scimitar
1026
1203
  else
1027
1204
  item&.to_s == value_item&.to_s
1028
1205
  end
1206
+
1207
+ if item_is_matched
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
+ array_attr_map = find_matching_static_attr_map(
1212
+ data: item,
1213
+ with_attr_map: attr_map_entry
1214
+ )
1215
+
1216
+ handled = clear_data_for_removal!(
1217
+ altering_hash: item,
1218
+ with_attr_map: array_attr_map
1219
+ )
1220
+
1221
+ handled ? item : nil
1222
+ else
1223
+ item
1224
+ end
1029
1225
  end
1226
+
1227
+ altering_hash[path_component].compact!
1228
+ end
1229
+
1230
+ elsif altering_hash[path_component].is_a?(Array)
1231
+ handled = false
1232
+ attr_map_path = path[..-2] + [path_component]
1233
+ attr_map_entry = with_attr_map.dig(*attr_map_path.map(&:to_sym))
1234
+
1235
+ if attr_map_entry.is_a?(Array) # Array mapping
1236
+ altering_hash[path_component].each do | data_to_check |
1237
+ array_attr_map = find_matching_static_attr_map(
1238
+ data: data_to_check,
1239
+ with_attr_map: attr_map_entry
1240
+ )
1241
+
1242
+ handled = clear_data_for_removal!(
1243
+ altering_hash: data_to_check,
1244
+ with_attr_map: array_attr_map
1245
+ )
1246
+ end
1247
+ end
1248
+
1249
+ if handled == false
1250
+ altering_hash[path_component] = []
1030
1251
  end
1252
+
1031
1253
  else
1032
- altering_hash.delete(path_component)
1254
+ altering_hash[path_component] = nil
1033
1255
  end
1034
1256
 
1035
1257
  end
@@ -1108,6 +1330,177 @@ module Scimitar
1108
1330
  end
1109
1331
  end
1110
1332
 
1333
+ # Static attribute maps are used where SCIM attributes include some
1334
+ # kind of array, but it's not an arbitrary collection (dynamic maps
1335
+ # handle those). Instead, specific matched values inside the SCIM
1336
+ # data are mapped to specific attributes in the local data model.
1337
+ #
1338
+ # A typical example is for e-mails, where the SCIM "type" field in an
1339
+ # array of e-mail addresses might get mapped to detect specific types
1340
+ # of address such as "work" and "home", which happen to be stored
1341
+ # locally in dedicated attributes (e.g. "work_email_address").
1342
+ #
1343
+ # During certain processing operations we end up with a set of data
1344
+ # sent in from some SCIM operation and need to make modifications
1345
+ # (e.g. for a PATCH) that require the attribute map corresponding to
1346
+ # each part of the inbound SCIM data to be known. That's where this
1347
+ # method comes in. Usually, it's not hard to traverse a path of SCIM
1348
+ # data and dig a corresponding path through the attribute map Hash,
1349
+ # except for static arrays. There, we need to know which of the
1350
+ # static map entries matches a piece of SCIM data *from entries* in
1351
+ # the array of SCIM data corresponding to the static map.
1352
+ #
1353
+ # Call here with a piece of SCIM data from an array, along with an
1354
+ # attribute map fragment that must be the Array containing mappings.
1355
+ # Static mapping entries from this are compared with the data and if
1356
+ # a match is found, the sub-attribute map from the static entry's
1357
+ # <tt>:using</tt> key is returned; else +nil+.
1358
+ #
1359
+ # Named parameters are:
1360
+ #
1361
+ # +data+:: A SCIM data entry from a SCIM data array which is
1362
+ # mapped via the data given in the +with_attr_map+
1363
+ # parameter.
1364
+ #
1365
+ # +with_attr_map+:: The attributes map fragment which must be an
1366
+ # Array of mappings for the corresponding array
1367
+ # in the SCIM data from which +data+ was drawn.
1368
+ #
1369
+ # For example, if SCIM data consisted of:
1370
+ #
1371
+ # {
1372
+ # 'emails' => [
1373
+ # {
1374
+ # 'type' => 'work',
1375
+ # 'value' => 'work_1@test.com'
1376
+ # },
1377
+ # {
1378
+ # 'type' => 'work',
1379
+ # 'value' => 'work_2@test.com'
1380
+ # }
1381
+ # ]
1382
+ # }
1383
+ #
1384
+ # ...which was mapped to the local data model using the following
1385
+ # attribute map:
1386
+ #
1387
+ # {
1388
+ # emails: [
1389
+ # { match: 'type', with: 'home', using: { value: :home_email } },
1390
+ # { match: 'type', with: 'work', using: { value: :work_email } },
1391
+ # ]
1392
+ # }
1393
+ #
1394
+ # ...then when it came to processing the SCIM 'emails' entry, one of
1395
+ # the array _entries_ therein would be passed in +data+, while the
1396
+ # attribute map's <tt>:emails</tt> key's value (the _array_ of map
1397
+ # data) would be given in <tt>:with_attr_map</tt>. The first SCIM
1398
+ # array entry matches +work+ so the <tt>:using</tt> part of the map
1399
+ # for that match would be returned:
1400
+ #
1401
+ # { value: :work_email }
1402
+ #
1403
+ # If there was a SCIM entry with a type of something unrecognised,
1404
+ # such as 'holday', then +nil+ would be returned since there is no
1405
+ # matching attribute map entry.
1406
+ #
1407
+ # Note that the <tt>:with_attr_map</tt> array can contain dynamic
1408
+ # mappings or even be just a simple fixed array - only things that
1409
+ # "look like" static mapping entries are processed (i.e. Hashes with
1410
+ # a Symbol key of <tt>:match</tt> present), with the rest ignored.
1411
+ #
1412
+ def find_matching_static_attr_map(data:, with_attr_map:)
1413
+ matched_map = with_attr_map.find do | static_or_dynamic_mapping |
1414
+
1415
+ # Only interested in Static Array mappings.
1416
+ #
1417
+ if static_or_dynamic_mapping.is_a?(Hash) && static_or_dynamic_mapping.key?(:match)
1418
+
1419
+ attr_to_match = static_or_dynamic_mapping[:match].to_s
1420
+ value_to_match = static_or_dynamic_mapping[:with]
1421
+ sub_attrs_map = static_or_dynamic_mapping[:using]
1422
+
1423
+ # If this mapping refers to the matched data at hand,
1424
+ # then we can process it further (see later below.
1425
+ #
1426
+ found = data[attr_to_match] == value_to_match
1427
+
1428
+ # Not found? No static map match perhaps; this could be
1429
+ # because a filter worked on a value which is fixed in
1430
+ # the static map. For example, a filter might check for
1431
+ # emails with "primary true", and the emergence of the
1432
+ # value for "primary" might not be in the data model -
1433
+ # it could be a constant declared in the 'using' part
1434
+ # of a static map. Ugh! Check for that.
1435
+ #
1436
+ unless found
1437
+ sub_attrs_map.each do | scim_attr, model_attr_or_constant |
1438
+
1439
+ # Only want constants such as 'true' or 'false'.
1440
+ #
1441
+ next if model_attr_or_constant.is_a?(Symbol)
1442
+
1443
+ # Does the static value match in the source data?
1444
+ # E.g. a SCIM attribute :primary with value 'true'.
1445
+ #
1446
+ if data[scim_attr] == model_attr_or_constant
1447
+ found = true
1448
+ break
1449
+ end
1450
+ end
1451
+ end
1452
+
1453
+ found
1454
+ else
1455
+ false
1456
+ end
1457
+ end
1458
+
1459
+ return matched_map&.dig(:using)
1460
+ end
1461
+
1462
+ # Related to #find_matching_static_attr_map - often, the reason to
1463
+ # find a static array entry related to some inbound SCIM data is for
1464
+ # a removal operation, where the way to "remove" the data in the
1465
+ # local data model is to set an attribute to "nil". This means you
1466
+ # need to know if there is an attribute writer related to the SCIM
1467
+ # data being removed - and #find_matching_static_attr_map helps.
1468
+ #
1469
+ # With that done, you can call here with the hash data to be changed
1470
+ # and fragment of attribute map that #find_matching_static_attr_map
1471
+ # (or something like it) found.
1472
+ #
1473
+ # +altering_hash+:: The fragment of SCIM data that might be updated
1474
+ # with +nil+ to ultimately lead to an atttribute
1475
+ # writer identified through +with_attr_map+ being
1476
+ # called with that value. This is often the same
1477
+ # that was passed in the +data+ attribute in a
1478
+ # prior #find_matching_static_attr_map call.
1479
+ #
1480
+ # +with_attr_map:: The map fragment that corresponds exactly to the
1481
+ # +altering_hash+ data - e.g. the return value of a
1482
+ # prior #find_matching_static_attr_map call.
1483
+ #
1484
+ # Update +altering_hash+ in place if the map finds a relevant local
1485
+ # data model attribute and returns +true+. If no changes are made,
1486
+ # returns +false+.
1487
+ #
1488
+ def clear_data_for_removal!(altering_hash:, with_attr_map:)
1489
+ handled = false
1490
+
1491
+ with_attr_map&.each do | scim_attr, model_attr_or_constant |
1492
+
1493
+ # Only process attribute names, not constants.
1494
+ #
1495
+ next unless model_attr_or_constant.is_a?(Symbol)
1496
+
1497
+ altering_hash[scim_attr] = nil
1498
+ handled = true
1499
+ end
1500
+
1501
+ return handled
1502
+ end
1503
+
1111
1504
  end # "included do"
1112
1505
  end # "module Mixin"
1113
1506
  end # "module Resources"