rom-ldap 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +251 -0
  3. data/CONTRIBUTING.md +18 -0
  4. data/README.md +172 -0
  5. data/TODO.md +33 -0
  6. data/config/responses.yml +328 -0
  7. data/lib/dry/monitor/ldap/colorizers/default.rb +17 -0
  8. data/lib/dry/monitor/ldap/colorizers/rouge.rb +31 -0
  9. data/lib/dry/monitor/ldap/logger.rb +58 -0
  10. data/lib/rom-ldap.rb +1 -0
  11. data/lib/rom/ldap.rb +22 -0
  12. data/lib/rom/ldap/alias.rb +30 -0
  13. data/lib/rom/ldap/associations.rb +6 -0
  14. data/lib/rom/ldap/associations/core.rb +23 -0
  15. data/lib/rom/ldap/associations/many_to_many.rb +18 -0
  16. data/lib/rom/ldap/associations/many_to_one.rb +22 -0
  17. data/lib/rom/ldap/associations/one_to_many.rb +32 -0
  18. data/lib/rom/ldap/associations/one_to_one.rb +19 -0
  19. data/lib/rom/ldap/associations/self_ref.rb +35 -0
  20. data/lib/rom/ldap/attribute.rb +327 -0
  21. data/lib/rom/ldap/client.rb +185 -0
  22. data/lib/rom/ldap/client/authentication.rb +118 -0
  23. data/lib/rom/ldap/client/operations.rb +233 -0
  24. data/lib/rom/ldap/commands.rb +6 -0
  25. data/lib/rom/ldap/commands/create.rb +41 -0
  26. data/lib/rom/ldap/commands/delete.rb +17 -0
  27. data/lib/rom/ldap/commands/update.rb +35 -0
  28. data/lib/rom/ldap/constants.rb +193 -0
  29. data/lib/rom/ldap/dataset.rb +286 -0
  30. data/lib/rom/ldap/dataset/conversion.rb +62 -0
  31. data/lib/rom/ldap/dataset/dsl.rb +299 -0
  32. data/lib/rom/ldap/dataset/persistence.rb +44 -0
  33. data/lib/rom/ldap/directory.rb +126 -0
  34. data/lib/rom/ldap/directory/capabilities.rb +71 -0
  35. data/lib/rom/ldap/directory/entry.rb +200 -0
  36. data/lib/rom/ldap/directory/env.rb +155 -0
  37. data/lib/rom/ldap/directory/operations.rb +282 -0
  38. data/lib/rom/ldap/directory/password.rb +122 -0
  39. data/lib/rom/ldap/directory/root.rb +187 -0
  40. data/lib/rom/ldap/directory/tokenization.rb +66 -0
  41. data/lib/rom/ldap/directory/transactions.rb +31 -0
  42. data/lib/rom/ldap/directory/vendors/active_directory.rb +129 -0
  43. data/lib/rom/ldap/directory/vendors/apache_ds.rb +27 -0
  44. data/lib/rom/ldap/directory/vendors/e_directory.rb +16 -0
  45. data/lib/rom/ldap/directory/vendors/open_directory.rb +12 -0
  46. data/lib/rom/ldap/directory/vendors/open_dj.rb +25 -0
  47. data/lib/rom/ldap/directory/vendors/open_ldap.rb +35 -0
  48. data/lib/rom/ldap/directory/vendors/three_eight_nine.rb +16 -0
  49. data/lib/rom/ldap/directory/vendors/unknown.rb +22 -0
  50. data/lib/rom/ldap/dsl.rb +76 -0
  51. data/lib/rom/ldap/errors.rb +47 -0
  52. data/lib/rom/ldap/expression.rb +77 -0
  53. data/lib/rom/ldap/expression_encoder.rb +174 -0
  54. data/lib/rom/ldap/extensions.rb +50 -0
  55. data/lib/rom/ldap/extensions/active_support_notifications.rb +26 -0
  56. data/lib/rom/ldap/extensions/compatibility.rb +11 -0
  57. data/lib/rom/ldap/extensions/dsml.rb +165 -0
  58. data/lib/rom/ldap/extensions/msgpack.rb +23 -0
  59. data/lib/rom/ldap/extensions/optimised_json.rb +25 -0
  60. data/lib/rom/ldap/extensions/rails_log_subscriber.rb +38 -0
  61. data/lib/rom/ldap/formatter.rb +26 -0
  62. data/lib/rom/ldap/functions.rb +207 -0
  63. data/lib/rom/ldap/gateway.rb +145 -0
  64. data/lib/rom/ldap/ldif.rb +74 -0
  65. data/lib/rom/ldap/ldif/exporter.rb +77 -0
  66. data/lib/rom/ldap/ldif/importer.rb +95 -0
  67. data/lib/rom/ldap/mapper_compiler.rb +19 -0
  68. data/lib/rom/ldap/matchers.rb +69 -0
  69. data/lib/rom/ldap/message_queue.rb +7 -0
  70. data/lib/rom/ldap/oid.rb +101 -0
  71. data/lib/rom/ldap/parsers/abstract_syntax.rb +91 -0
  72. data/lib/rom/ldap/parsers/attribute.rb +290 -0
  73. data/lib/rom/ldap/parsers/filter_syntax.rb +133 -0
  74. data/lib/rom/ldap/pdu.rb +285 -0
  75. data/lib/rom/ldap/plugin/pagination.rb +145 -0
  76. data/lib/rom/ldap/plugins.rb +7 -0
  77. data/lib/rom/ldap/projection_dsl.rb +38 -0
  78. data/lib/rom/ldap/relation.rb +135 -0
  79. data/lib/rom/ldap/relation/exporting.rb +72 -0
  80. data/lib/rom/ldap/relation/reading.rb +461 -0
  81. data/lib/rom/ldap/relation/writing.rb +64 -0
  82. data/lib/rom/ldap/responses.rb +17 -0
  83. data/lib/rom/ldap/restriction_dsl.rb +45 -0
  84. data/lib/rom/ldap/schema.rb +123 -0
  85. data/lib/rom/ldap/schema/attributes_inferrer.rb +59 -0
  86. data/lib/rom/ldap/schema/dsl.rb +13 -0
  87. data/lib/rom/ldap/schema/inferrer.rb +50 -0
  88. data/lib/rom/ldap/schema/type_builder.rb +133 -0
  89. data/lib/rom/ldap/scope.rb +19 -0
  90. data/lib/rom/ldap/search_request.rb +249 -0
  91. data/lib/rom/ldap/socket.rb +210 -0
  92. data/lib/rom/ldap/tasks/ldap.rake +103 -0
  93. data/lib/rom/ldap/tasks/ldif.rake +80 -0
  94. data/lib/rom/ldap/transaction.rb +29 -0
  95. data/lib/rom/ldap/type_map.rb +88 -0
  96. data/lib/rom/ldap/types.rb +158 -0
  97. data/lib/rom/ldap/version.rb +17 -0
  98. data/lib/rom/plugins/relation/ldap/active_directory.rb +182 -0
  99. data/lib/rom/plugins/relation/ldap/auto_restrictions.rb +69 -0
  100. data/lib/rom/plugins/relation/ldap/e_directory.rb +27 -0
  101. data/lib/rom/plugins/relation/ldap/instrumentation.rb +35 -0
  102. data/lib/rouge/lexers/ldap.rb +72 -0
  103. data/lib/rouge/themes/ldap.rb +49 -0
  104. metadata +231 -0
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rom/plugins/relation/ldap/instrumentation'
4
+ require 'rom/plugins/relation/ldap/auto_restrictions'
5
+ require 'rom/plugins/relation/ldap/active_directory'
6
+ require 'rom/plugins/relation/ldap/e_directory'
7
+ require 'rom/ldap/plugin/pagination'
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rom/ldap/dsl'
4
+
5
+ module ROM
6
+ module LDAP
7
+ # Projection DSL used in reading API (`select`, `select_append` etc.)
8
+ #
9
+ # @see LDAP::Schema#project
10
+ #
11
+ # @api public
12
+ class ProjectionDSL < DSL
13
+
14
+ # @api private
15
+ def respond_to_missing?(name, include_private = false)
16
+ super || type(name)
17
+ end
18
+
19
+ private
20
+
21
+ # @api private
22
+ def method_missing(meth, *args, &block)
23
+ if schema.key?(meth)
24
+ schema[meth]
25
+ else
26
+ type = type(meth)
27
+
28
+ if type
29
+ ::ROM::LDAP::Attribute[type].value(args[0])
30
+ else
31
+ super
32
+ end
33
+ end
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rom/ldap/types'
4
+ require 'rom/ldap/schema'
5
+ require 'rom/ldap/dataset'
6
+ require 'rom/ldap/attribute'
7
+ require 'rom/ldap/relation/reading'
8
+ require 'rom/ldap/relation/writing'
9
+ require 'rom/ldap/relation/exporting'
10
+ require 'rom/ldap/transaction'
11
+
12
+ module ROM
13
+ module LDAP
14
+ class Relation < ROM::Relation
15
+
16
+ adapter :ldap
17
+
18
+ include LDAP
19
+ include Reading
20
+ include Writing
21
+ include Exporting
22
+
23
+ # @!method self.base
24
+ # Per relation override for the search base.
25
+ #
26
+ # @overload base
27
+ # Return search base value
28
+ # @return [String]
29
+ #
30
+ # @overload base(value)
31
+ # Set search base value
32
+ defines :base
33
+
34
+ # @!method self.branches
35
+ # Alternative search bases by name.
36
+ #
37
+ # @overload branches
38
+ # Return hash of search branches.
39
+ # @return [Hash]
40
+ #
41
+ # @overload branches(value)
42
+ # Set hash of search branches.
43
+ defines :branches
44
+ branches EMPTY_HASH
45
+
46
+ extend Notifications::Listener
47
+
48
+ subscribe('configuration.relations.schema.set', adapter: :ldap) do |event|
49
+ relation = event[:relation]
50
+ relation.dataset do
51
+ # @return [Dataset]
52
+ #
53
+ # @override Dataset#base
54
+ #
55
+ # Set dataset search base using either class-level value or gateway config.
56
+ with(base: relation.base || directory.base)
57
+ end
58
+ end
59
+
60
+ schema_class LDAP::Schema
61
+ schema_attr_class LDAP::Attribute
62
+ schema_inferrer LDAP::Schema::Inferrer.new.freeze
63
+ schema_dsl LDAP::Schema::DSL
64
+
65
+ forward(*Dataset.dsl)
66
+
67
+ # Fallsback to 'entrydn' operational value.
68
+ #
69
+ # @return [Symbol]
70
+ #
71
+ # @api public
72
+ def primary_key
73
+ attribute = schema.find(&:primary_key?)
74
+
75
+ if attribute
76
+ attribute.alias || attribute.name
77
+ else
78
+ DEFAULT_PK
79
+ end
80
+ end
81
+
82
+ # Expose the search base currently in use.
83
+ #
84
+ # @return [String] current base
85
+ #
86
+ # @api public
87
+ def base
88
+ dataset.opts[:base]
89
+ end
90
+
91
+ # Current dataset in LDAP filter format.
92
+ #
93
+ # @return [String]
94
+ #
95
+ # @api public
96
+ def to_filter
97
+ dataset.opts[:filter]
98
+ end
99
+
100
+ # @api public
101
+ def self.associations
102
+ schema.associations
103
+ end
104
+
105
+ # @return [Relation]
106
+ #
107
+ # @api public
108
+ def assoc(name)
109
+ associations[name].call
110
+ end
111
+
112
+ # LDAP Transactions (LDAPTXN) is an experimental RFC.
113
+ # The latest revision can be found at http://tools.ietf.org/rfc/rfc5805.txt
114
+ #
115
+ # @see https://directory.fedoraproject.org/docs/389ds/design/ldap-transactions.html
116
+ #
117
+ # @yield [t] Transaction
118
+ #
119
+ # @return [Mixed]
120
+ #
121
+ # @api public
122
+ def transaction(opts = EMPTY_OPTS, &block)
123
+ Transaction.new(dataset.directory).run(opts, &block)
124
+ end
125
+
126
+ #
127
+ # @api private
128
+ # def join(source_table, join_keys)
129
+ # binding.pry
130
+ # __registry__[source_table].where(join_keys)
131
+ # end
132
+
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'json'
5
+ require 'rom/ldap/ldif'
6
+
7
+ module ROM
8
+ module LDAP
9
+ class Relation < ROM::Relation
10
+
11
+ # LDIF, JSON, YAML and if loading extensions MsgPack and DSML.
12
+ #
13
+ module Exporting
14
+ using LDIF
15
+
16
+ # Export the relation as LDIF
17
+ #
18
+ # @return [String]
19
+ #
20
+ # @example
21
+ # relation.to_ldif
22
+ #
23
+ # @api public
24
+ def to_ldif
25
+ export.to_ldif
26
+ end
27
+
28
+ # Export the relation as JSON
29
+ #
30
+ # @param _opts [Mixed] compatibility with JSON.generate
31
+ #
32
+ # @return [String]
33
+ #
34
+ # @example
35
+ # relation.to_json
36
+ # JSON.generate(relation)
37
+ #
38
+ # @api public
39
+ def to_json(_opts = nil)
40
+ export.to_json
41
+ end
42
+
43
+ # Export the relation as YAML
44
+ #
45
+ # @return [String]
46
+ #
47
+ # @example
48
+ # relation.to_yaml
49
+ #
50
+ # @api public
51
+ def to_yaml
52
+ export.to_yaml
53
+ end
54
+
55
+ private
56
+
57
+ # Serialize the selected dataset attributes in a formatted string.
58
+ #
59
+ # @example i.e. YAML, JSON, LDIF, BINARY
60
+ # #=> relation.export.to_format
61
+ #
62
+ # @return [Hash, Array<Hash>]
63
+ #
64
+ # @api public
65
+ def export
66
+ dataset.respond_to?(:export) ? dataset.export : dataset
67
+ end
68
+ end
69
+
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,461 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ROM
4
+ module LDAP
5
+ class Relation < ROM::Relation
6
+
7
+ module Reading
8
+ # Specify an alternative search base.
9
+ #
10
+ # @example
11
+ # relation.with_base("cn=department,ou=users,dc=org")
12
+ #
13
+ # @return [Relation] Defaults to class attribute
14
+ #
15
+ # @api public
16
+ def with_base(alt_base)
17
+ new(dataset.with(base: alt_base))
18
+ end
19
+
20
+ # Change the search base to search the whole directory tree.
21
+ #
22
+ # @example
23
+ # relation.whole_tree
24
+ #
25
+ # @return [Relation]
26
+ #
27
+ # @api public
28
+ def whole_tree
29
+ with_base(EMPTY_STRING)
30
+ end
31
+
32
+ # An alternative search base selected from a class level hash.
33
+ #
34
+ # @param key [Symbol]
35
+ #
36
+ # @example
37
+ # Relation.branches { custom: '(attribute=value)' }
38
+ #
39
+ # relation.branch(:custom)
40
+ #
41
+ # @api public
42
+ def branch(key)
43
+ with_base(self.class.branches[key])
44
+ end
45
+
46
+ # Remove additional search criteria and return to initial filter.
47
+ #
48
+ # @return [Relation]
49
+ #
50
+ # @api public
51
+ def unfiltered
52
+ new(dataset.unfiltered)
53
+ end
54
+
55
+ # Include internal operational attributes in the tuples.
56
+ #
57
+ # @return [Relation]
58
+ #
59
+ # @api public
60
+ def operational
61
+ new(dataset.with(attrs: ALL_ATTRS))
62
+ end
63
+
64
+ # Replace the relation filter with a new query.
65
+ #
66
+ # @param new_filter [String] Valid LDAP filter string
67
+ #
68
+ # @return [Relation]
69
+ #
70
+ # @api public
71
+ def search(new_filter)
72
+ new(dataset.with(name: new_filter))
73
+ end
74
+
75
+ # Returns True if the filtered entity can bind.
76
+ #
77
+ # @return [Boolean]
78
+ #
79
+ # @api public
80
+ def authenticate(password)
81
+ dataset.bind(password)
82
+ end
83
+
84
+ # Map tuples from the relation
85
+ #
86
+ # @example
87
+ # users.map { |user| user[:id] }
88
+ # # => [1, 2, 3]
89
+ #
90
+ # users.map(:id).to_a
91
+ # # => [1, 2, 3]
92
+ #
93
+ # @param key [Symbol] An optional name of the key for extracting values
94
+ # from tuples
95
+ #
96
+ # @return [Array<Array>]
97
+ #
98
+ # @api public
99
+ def map(key = nil, &block)
100
+ dataset.map(key, &block)
101
+ end
102
+
103
+ # Array of values for an :attribute from all tuples.
104
+ #
105
+ # @param field [Symbol] formatted or canonical attribute key
106
+ #
107
+ # @example
108
+ # relation.by_sn('Hamilton').list(:given_name)
109
+ #
110
+ # @return [Array<Mixed>]
111
+ #
112
+ # @raise [ROM::Struct::MissingAttribute] If auto_struct? and field not present.
113
+ #
114
+ # @api public
115
+ def list(field)
116
+ if auto_struct?
117
+ to_a.flat_map(&field)
118
+ else
119
+ map(field).to_a.compact.flatten
120
+ end
121
+ end
122
+
123
+ # Count the number of entries selected from the paginated dataset.
124
+ #
125
+ # @return [Integer]
126
+ #
127
+ # @api public
128
+ def count
129
+ dataset.__send__(__method__)
130
+ end
131
+
132
+ # Count the number of entries in the dataset.
133
+ #
134
+ # @return [Integer]
135
+ #
136
+ # @api public
137
+ def total
138
+ dataset.__send__(__method__)
139
+ end
140
+
141
+ # @return [Boolean]
142
+ #
143
+ # @api public
144
+ def one?
145
+ dataset.__send__(__method__)
146
+ end
147
+ alias_method :distinct?, :one?
148
+ alias_method :unique?, :one?
149
+
150
+ # @return [Boolean]
151
+ #
152
+ # @api public
153
+ def any?(&block)
154
+ dataset.__send__(__method__, &block)
155
+ end
156
+ alias_method :exist?, :any?
157
+
158
+ # @return [Boolean]
159
+ #
160
+ # @api public
161
+ def none?(&block)
162
+ dataset.__send__(__method__, &block)
163
+ end
164
+
165
+ # @return [Boolean]
166
+ #
167
+ # @api public
168
+ def all?(&block)
169
+ dataset.__send__(__method__, &block)
170
+ end
171
+
172
+ # Find tuples by primary_key which defaults to :entry_dn
173
+ # Method is required by commands.
174
+ #
175
+ # @param pks [Integer, String]
176
+ #
177
+ # @example
178
+ # relation.by_pk(1001, 1002, 1003, 1004)
179
+ # relation.by_pk('uid=test1,ou=users,dc=example,dc=com')
180
+ #
181
+ # @return [Relation]
182
+ def by_pk(*pks)
183
+ where(primary_key => pks)
184
+ end
185
+
186
+ # Fetch a tuple identified by the pk
187
+ #
188
+ # @param pk [String, Integer]
189
+ #
190
+ # @example
191
+ # users.fetch(1001) # => {:id => 1, name: "Peter"}
192
+ #
193
+ # @return [Hash]
194
+ #
195
+ # @raise [ROM::TupleCountMismatchError] When 0 or more than 1 tuples were found
196
+ #
197
+ # @api public
198
+ def fetch(*pk)
199
+ by_pk(*pk).one!
200
+ end
201
+
202
+ # First tuple from the relation
203
+ #
204
+ # @example
205
+ # relation.where(sn: 'smith').first
206
+ #
207
+ # @return [Hash]
208
+ #
209
+ # @api public
210
+ def first
211
+ dataset.first.to_h
212
+ end
213
+
214
+ # Last tuple from the relation
215
+ #
216
+ # @example
217
+ # relation.where(sn: 'smith').last
218
+ #
219
+ # @return [Hash]
220
+ #
221
+ # @api public
222
+ def last
223
+ dataset.reverse_each.first.to_h
224
+ end
225
+
226
+ # Use server-side sorting if available.
227
+ #
228
+ # Orders the dataset by a given attribute using the coerced value.
229
+ #
230
+ # SortResult ::= SEQUENCE {
231
+ # sortResult ENUMERATED {
232
+ # success (0), -- results are sorted
233
+ # operationsError (1), -- server internal failure
234
+ # timeLimitExceeded (3), -- timelimit reached before
235
+ # -- sorting was completed
236
+ # strongAuthRequired (8), -- refused to return sorted
237
+ # -- results via insecure
238
+ # -- protocol
239
+ # adminLimitExceeded (11), -- too many matching entries
240
+ # -- for the server to sort
241
+ # noSuchAttribute (16), -- unrecognized attribute
242
+ # -- type in sort key
243
+ # inappropriateMatching (18), -- unrecognized or inappro-
244
+ # -- priate matching rule in
245
+ # -- sort key
246
+ # insufficientAccessRights (50), -- refused to return sorted
247
+ # -- results to this client
248
+ # busy (51), -- too busy to process
249
+ # unwillingToPerform (53), -- unable to sort
250
+ # other (80)
251
+ # },
252
+ # attributeType [0] AttributeType OPTIONAL }
253
+ #
254
+ # @param attribute [Symbol]
255
+ #
256
+ # @example
257
+ # relation.order(:uid_number).to_a =>
258
+ # [
259
+ # {uid_number: 101},
260
+ # {uid_number: 202},
261
+ # {uid_number: 303}
262
+ # ]
263
+ #
264
+ # @return [Relation]
265
+ #
266
+ # @see https://tools.ietf.org/html/rfc2891
267
+ #
268
+ # @api public
269
+ def order(*attribute)
270
+ new(dataset.with(sort_attrs: attribute))
271
+ end
272
+
273
+ # Reverses the dataset.
274
+ # Use server-side sorting if available.
275
+ #
276
+ # @example
277
+ # relation.reverse
278
+ #
279
+ # @return [Relation]
280
+ #
281
+ # @api public
282
+ def reverse
283
+ new(dataset.with(direction: :desc))
284
+ end
285
+
286
+ # Limits the dataset to a number of tuples
287
+ #
288
+ # @example
289
+ # relation.limit(6)
290
+ #
291
+ # @return [Relation]
292
+ #
293
+ # @api public
294
+ def limit(number)
295
+ new(dataset.with(limit: number))
296
+ end
297
+
298
+ # Shuffles the dataset.
299
+ #
300
+ # @example
301
+ # relation.random
302
+ #
303
+ # @return [Relation]
304
+ #
305
+ # @api public
306
+ def random
307
+ new(dataset.with(random: true))
308
+ end
309
+
310
+ # Searches attributes of the projected schema for a match.
311
+ #
312
+ # @param value [String]
313
+ #
314
+ # @return [Relation]
315
+ #
316
+ # @example
317
+ # relation.find('eo')
318
+ #
319
+ # @api public
320
+ def grep(value)
321
+ meta_fields = schema.attributes.select { |a| a.meta[:grep] }
322
+ fields = meta_fields.any? ? meta_fields : schema
323
+
324
+ new(dataset.grep(fields.map(&:name).sort, value))
325
+ end
326
+ alias_method :find, :grep
327
+
328
+ # Overwrites forwarding to Dataset#where
329
+ #
330
+ # A Hash argument is passed straight to Dataset#equal.
331
+ # Otherwise the RestrictionDSL builds abstract queries
332
+ #
333
+ # @param args [Array<Array, Hash>] AST queries or an attr/val hash.
334
+ #
335
+ #
336
+ # @example
337
+ # users.where { id.is(1) }
338
+ # users.where { id == 1 }
339
+ # users.where { id > 1 }
340
+ # users.where { id.gte(1) }
341
+ #
342
+ # users.where(users[:id].is(1))
343
+ # users.where(users[:id].lt(1))
344
+ #
345
+ # @api public
346
+ def where(*args, &block)
347
+ if block
348
+ where(args).where(schema.restriction(&block))
349
+ elsif args.size == 1 && args[0].is_a?(Hash)
350
+ new(dataset.equal(args[0]))
351
+ elsif !args.empty?
352
+ new(dataset.join(args))
353
+ else
354
+ self
355
+ end
356
+ end
357
+
358
+ # Pluck value(s) from specific attribute(s)
359
+ # unwrapped only if all are lone results.
360
+ #
361
+ # @example Single value
362
+ # users.pluck(:uidnumber)
363
+ # # ["1", "2"]
364
+ #
365
+ # users.pluck(:cn)
366
+ # # [["Cat", "House Cat"], ["Mouse"]]
367
+ #
368
+ # @example Multiple values
369
+ # users.pluck(:gidnumber, :uid)
370
+ # # [["1", "Jane"] ["2", "Joe"]]
371
+ #
372
+ # @param names [Symbol, String, Array<String, Symbol>]
373
+ #
374
+ # @return [Array<String, Array>]
375
+ #
376
+ # @api public
377
+ def pluck(*names)
378
+ raise ArgumentError, 'no attributes provided' if names.empty?
379
+
380
+ map do |entry|
381
+ results = values = names.map { |n| entry[n] }
382
+ results = values.map(&:pop) if values.map(&:one?).all?
383
+ results.one? ? results.pop : results
384
+ end
385
+ end
386
+
387
+ # Returns tuples with popped values.
388
+ #
389
+ # @return [LDAP::Relation]
390
+ #
391
+ def unwrap
392
+ new Functions[:map_array, Functions[:map_values, :pop]].call(self)
393
+ end
394
+
395
+ # Select specific attributes
396
+ #
397
+ # @overload select(*attributes)
398
+ # Project relation using schema attributes
399
+ #
400
+ # @example using attributes
401
+ # users.select(:id, :name).first
402
+ # # {:id => 1, :name => "Jane"}
403
+ #
404
+ # @example using schema
405
+ # users.select(*schema.project(:id)).first
406
+ # # {:id => 1}
407
+ #
408
+ # @param [Array<LDAP::Attribute>] columns A list of schema attributes
409
+ #
410
+ # @overload select(&block)
411
+ # Project relation using projection DSL
412
+ #
413
+ # @example using attributes
414
+ # users.select { cn.as(:user_name) }
415
+ # # {:user_name => "Peter Hamilton"}
416
+ #
417
+ # users.select { [uidnumber, sn] }
418
+ # # {:uidnumber => 501, :name => "Hamilton"}
419
+ #
420
+ # @param [Array<LDAP::Attribute>] columns A list of schema attributes
421
+ #
422
+ # @return [Relation]
423
+ #
424
+ # @api public
425
+ def select(*args, &block)
426
+ schema.project(*args, &block).call(self)
427
+ end
428
+ alias_method :project, :select
429
+
430
+ # Rename attributes in a relation
431
+ #
432
+ # This method is intended to be used internally within a relation object
433
+ #
434
+ # @example
435
+ # users.rename(name: :user_name).first
436
+ # # {:id => 1, :user_name => "Jane", ... }
437
+ #
438
+ # @param mapping [Hash<Symbol=>Symbol>] A name => new_name map
439
+ #
440
+ # @return [Relation]
441
+ #
442
+ # @api public
443
+ def rename(mapping)
444
+ schema.rename(mapping).call(self)
445
+ end
446
+
447
+ # Append specific columns to select clause
448
+ #
449
+ # @see Relation#select
450
+ #
451
+ # @return [Relation]
452
+ #
453
+ # @api public
454
+ def select_append(*args, &block)
455
+ schema.merge(schema.canonical.project(*args, &block)).call(self)
456
+ end
457
+ end
458
+
459
+ end
460
+ end
461
+ end