scimitar 1.8.1 → 1.8.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed2a0e01326248966fb009845e4fa05f4cae800b79bc3d85497960c833869f24
4
- data.tar.gz: c7f92d402444abf8b4df77d8b404c85ff90b06e113126ebafe75c8935608f869
3
+ metadata.gz: f83c7d7e476706bfc909fa2e08761aa544333d9fd32f077b0c453949f06cb8e9
4
+ data.tar.gz: a9c847ec6f3ebd6d16cb8efae0093bc6fca33e5e05975532aeb17983eed79d0d
5
5
  SHA512:
6
- metadata.gz: 834cbc912ea8cdf996cb74a57d4f24495b3e0b005924635829ddb5ab99025ca17bdec5aef58c79353af189bb3304c4560017b31c9089614af4cf4b00832909b9
7
- data.tar.gz: 71398ecaa9a879cd37fe41867526a824b9cc4db32efd8c3a23012e086a5509a95db66140f7e0c623bc3bde4ac648f5eca5f51bd99e5a7b2322b65d326ff7ea4d
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"