rom-ldap 0.2.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.
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