@@ -112,11 +112,19 @@ func (c *Connector) Type() string {
112112const maxValidateProbes = 5
113113
114114// Validate checks the application credentials and operator access, including
115- // node listing and a sample document read when one is visible in a workspace .
115+ // node listing and a sample document read when one is visible.
116116//
117- // The operator is not expected to reach every workspace in the tenant: app
118- // credentials are valid as long as one reachable workspace yields one readable
119- // document. A failed probe does not condemn that workspace; the walk keeps
117+ // A data source that selects resources is validated against those resources
118+ // only. They are what the operator asked to sync, so an unrelated workspace
119+ // that happens to be listed first, and that the operator cannot read, must not
120+ // decide whether the selection is usable: a selected folder or workspace is
121+ // walked for one readable document, and a selected document is probed
122+ // directly. Without a selection there is nothing narrower to sample, so the
123+ // whole tenant is walked as before.
124+ //
125+ // In both modes the operator is not expected to reach every object: app
126+ // credentials are valid as long as one reachable document can be read. A
127+ // failed probe does not condemn the object it was found in; the walk keeps
120128// looking, inside the probe cap, for another document the operator can read.
121129// When the cap itself stops a conclusive walk, the error says how many
122130// documents were read.
@@ -127,6 +135,15 @@ func (c *Connector) Validate(ctx context.Context, dataSourceConfig *types.DataSo
127135 }
128136 api := c .api (cfg )
129137 settings := parseDocumentSettings (dataSourceConfig )
138+ if selected := uniqueIDs (dataSourceConfig .ResourceIDs ); len (selected ) > 0 {
139+ return validateSelectedResources (ctx , api , selected , settings )
140+ }
141+ return validateTenantCredentials (ctx , api , settings )
142+ }
143+
144+ // validateTenantCredentials samples every workspace in the tenant, which is
145+ // what a data source without a selection is going to sync.
146+ func validateTenantCredentials (ctx context.Context , api dingTalkAPI , settings documentSettings ) error {
130147 workspaces , err := api .listWorkspaces (ctx )
131148 if err != nil {
132149 return fmt .Errorf ("validate DingTalk data source: %w" , err )
@@ -174,19 +191,168 @@ func (c *Connector) Validate(ctx context.Context, dataSourceConfig *types.DataSo
174191 break
175192 }
176193 }
177- // Folders were left unexplored, because the listing budget ran out or a
178- // folder could not be listed, so an unreadable document elsewhere does not
179- // prove the data source is unusable. A probe cap hit in that situation
180- // stays quiet too: the walk cannot claim it sampled every visible document.
194+ return validateOutcome (inconclusive , sawDocument , workspaceFail , probeCapHit , lastErr )
195+ }
196+
197+ // validateSelectedResources checks only the scopes a data source selects.
198+ // References are resolved through the same code path a sync uses, so Validate
199+ // answers the question the operator asked — would this selection sync
200+ // anything? — instead of sampling workspaces the selection never mentions.
201+ //
202+ // A reference this build cannot resolve is reported rather than quietly
203+ // replaced by the tenant walk: the data source would fail on that resource,
204+ // and a selection the picker can no longer resolve is exactly what the
205+ // operator needs to see.
206+ func validateSelectedResources (
207+ ctx context.Context ,
208+ api dingTalkAPI ,
209+ selected []string ,
210+ settings documentSettings ,
211+ ) error {
212+ if err := ctx .Err (); err != nil {
213+ return fmt .Errorf ("validate DingTalk data source: %w" , err )
214+ }
215+ workspaces , err := api .listWorkspaces (ctx )
216+ if err != nil {
217+ return fmt .Errorf ("validate DingTalk data source: %w" , err )
218+ }
219+ scopes , failures , err := resolveSyncScopes (ctx , api , workspaces , selected , settings )
220+ if err != nil {
221+ return fmt .Errorf ("validate DingTalk data source: %w" , err )
222+ }
223+ names := make (map [string ]string , len (workspaces ))
224+ for _ , item := range workspaces {
225+ names [item .ID ] = item .Name
226+ }
227+
228+ var (
229+ lastErr error
230+ resolveErr error
231+ sawDocument bool
232+ inconclusive bool
233+ scopeFail int
234+ probes int
235+ listings int
236+ probeCapHit bool
237+ )
238+ for _ , resourceID := range selected {
239+ failure := failures [resourceID ]
240+ if failure == nil {
241+ continue
242+ }
243+ scopeFail ++
244+ if resolveErr == nil {
245+ resolveErr = fmt .Errorf ("resource %s: %w" , resourceID , failure )
246+ }
247+ }
248+ for i , scope := range scopes {
249+ // A failed probe names the workspace it was found in, the way the
250+ // tenant-wide sample does, so it stays actionable.
251+ name := names [scope .Reference .WorkspaceID ]
252+ if name == "" {
253+ name = scope .Reference .WorkspaceID
254+ }
255+ readable , unexplored , hitCap , err := sampleScope (
256+ ctx , api , scope , name , settings ,
257+ & listings , & probes , & sawDocument , & lastErr ,
258+ )
259+ if err != nil {
260+ // Name the selected node when the selection is one: a folder that
261+ // cannot be listed is what the operator has to look at, and the
262+ // workspace alone would not say which folder.
263+ label := fmt .Sprintf ("workspace %q" , name )
264+ if scope .Reference .NodeID != "" {
265+ label = fmt .Sprintf ("workspace %q node %q" , name , scope .Reference .NodeID )
266+ }
267+ lastErr = fmt .Errorf ("%s: %w" , label , err )
268+ // A cancelled or timed-out request proves nothing about the
269+ // credentials, so it must never fall through to acceptance.
270+ if ctx .Err () != nil || isContextError (err ) {
271+ return fmt .Errorf ("validate DingTalk data source: %w" , lastErr )
272+ }
273+ scopeFail ++
274+ continue
275+ }
276+ if readable {
277+ return nil
278+ }
279+ if unexplored {
280+ inconclusive = true
281+ }
282+ if probes >= maxValidateProbes {
283+ probeCapHit = hitCap || i + 1 < len (scopes )
284+ break
285+ }
286+ }
287+ if lastErr == nil {
288+ // A failed probe names a workspace and a document the operator can act
289+ // on; a reference that does not resolve can only name itself.
290+ lastErr = resolveErr
291+ }
292+ return validateOutcome (inconclusive , sawDocument , scopeFail , probeCapHit , lastErr )
293+ }
294+
295+ // sampleScope probes one resolved selection scope. A selected document is read
296+ // through its own read API, while a selected folder or workspace is walked the
297+ // way the tenant sample walks a workspace root, so a readable document below
298+ // the selected folder is still found.
299+ func sampleScope (
300+ ctx context.Context ,
301+ api dingTalkAPI ,
302+ scope syncScope ,
303+ workspaceName string ,
304+ settings documentSettings ,
305+ listings * int ,
306+ probes * int ,
307+ sawDocument * bool ,
308+ lastErr * error ,
309+ ) (readable , unexplored , capHit bool , err error ) {
310+ if scope .Document == nil {
311+ return sampleWorkspace (
312+ ctx , api , workspaceName , scope .StartNodeID , settings ,
313+ listings , probes , sawDocument , lastErr ,
314+ )
315+ }
316+ document := * scope .Document
317+ * probes ++
318+ * sawDocument = true
319+ if probeErr := verifyDocument (ctx , api , document ); probeErr != nil {
320+ if ctx .Err () != nil || isContextError (probeErr ) {
321+ return false , false , false , fmt .Errorf ("document %q: %w" , document .title (), probeErr )
322+ }
323+ * lastErr = fmt .Errorf (
324+ "workspace %q document %q: %w" , workspaceName , document .title (), probeErr ,
325+ )
326+ return false , false , false , nil
327+ }
328+ return true , false , false , nil
329+ }
330+
331+ // validateOutcome folds the counters of a validate walk into its result. The
332+ // tenant-wide sample and the selection-scoped one both end here, because they
333+ // answer the same question: is there a document in what this data source syncs
334+ // that the operator can read?
335+ //
336+ // Folders were left unexplored, because the listing budget ran out or a folder
337+ // could not be listed, so an unreadable document elsewhere does not prove the
338+ // data source is unusable. A probe cap hit in that situation stays quiet too:
339+ // the walk cannot claim it sampled every visible document.
340+ //
341+ // Nothing readable anywhere. Only a scope that exposed no document at all is
342+ // accepted: there was nothing to read, so credentials could not be disproved.
343+ // A document that exists but cannot be read means the data source would sync
344+ // nothing, which is worth reporting.
345+ func validateOutcome (
346+ inconclusive bool ,
347+ sawDocument bool ,
348+ failures int ,
349+ probeCapHit bool ,
350+ lastErr error ,
351+ ) error {
181352 if inconclusive {
182353 return nil
183354 }
184-
185- // Nothing readable anywhere. Only a tenant that exposed no document at all
186- // is accepted: there was nothing to read, so credentials could not be
187- // disproved. A document that exists but cannot be read means the data
188- // source would sync nothing, which is worth reporting.
189- if ! sawDocument && workspaceFail == 0 {
355+ if ! sawDocument && failures == 0 {
190356 return nil
191357 }
192358 if lastErr == nil {
0 commit comments