You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
583 lines
19 KiB
583 lines
19 KiB
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// ServiceTrade attachment purpose constants with file type restrictions
|
|
const (
|
|
// AttachmentPurposeJobPaperwork - PDF documents accepted
|
|
AttachmentPurposeJobPaperwork = 1
|
|
|
|
// AttachmentPurposeJobVendorBill - PDF documents accepted
|
|
AttachmentPurposeJobVendorBill = 2
|
|
|
|
// AttachmentPurposeJobPicture - Only image files: *.gif, *.jpeg, *.jpg, *.png, *.tif, *.tiff
|
|
AttachmentPurposeJobPicture = 3
|
|
|
|
// AttachmentPurposeDeficiencyRepairProposal - PDF documents accepted
|
|
AttachmentPurposeDeficiencyRepairProposal = 5
|
|
|
|
// AttachmentPurposeGenericAttachment - PDF documents accepted
|
|
AttachmentPurposeGenericAttachment = 7
|
|
|
|
// AttachmentPurposeAvatarImage - Only image files: *.gif, *.jpeg, *.jpg, *.png, *.tif, *.tiff
|
|
AttachmentPurposeAvatarImage = 8
|
|
|
|
// AttachmentPurposeImport - Only text files: *.csv, *.txt
|
|
AttachmentPurposeImport = 9
|
|
|
|
// AttachmentPurposeBlankPaperwork - PDF documents accepted
|
|
AttachmentPurposeBlankPaperwork = 10
|
|
|
|
// AttachmentPurposeWorkAcknowledgement - Only JSON files: *.json
|
|
AttachmentPurposeWorkAcknowledgement = 11
|
|
|
|
// AttachmentPurposeLogo - Only image files: *.gif, *.jpeg, *.jpg, *.png, *.tif, *.tiff
|
|
AttachmentPurposeLogo = 12
|
|
|
|
// AttachmentPurposeJobInvoice - PDF documents accepted
|
|
AttachmentPurposeJobInvoice = 14
|
|
)
|
|
|
|
// UploadAttachment uploads a file as an attachment to a job
|
|
func (s *Session) UploadAttachment(jobID, filename, purpose string, fileContent []byte) (map[string]interface{}, error) {
|
|
url := fmt.Sprintf("%s/attachment", BaseURL)
|
|
|
|
// Create a buffer to hold the form data
|
|
var b bytes.Buffer
|
|
w := multipart.NewWriter(&b)
|
|
|
|
// Log received values
|
|
log.Printf("Uploading attachment to job ID: %s", jobID)
|
|
log.Printf("Filename: %s", filename)
|
|
log.Printf("Purpose value: '%s'", purpose)
|
|
log.Printf("File content length: %d bytes", len(fileContent))
|
|
|
|
// The ServiceTrade API expects the purpose ID as an integer
|
|
purposeStr := strings.TrimSpace(purpose)
|
|
|
|
// Try to parse the purpose as an integer, removing any leading zeros first
|
|
purposeStr = strings.TrimLeft(purposeStr, "0")
|
|
if purposeStr == "" {
|
|
purposeStr = "0" // If only zeros were provided
|
|
}
|
|
|
|
purposeInt, err := strconv.Atoi(purposeStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid purpose value '%s': must be a valid integer: %v", purpose, err)
|
|
}
|
|
|
|
log.Printf("Using purpose value: %d for job: %s", purposeInt, jobID)
|
|
|
|
// Add the purposeId (attachment type) as an integer
|
|
// NOTE: The API expects "purposeId", not "purpose"
|
|
if err := w.WriteField("purposeId", fmt.Sprintf("%d", purposeInt)); err != nil {
|
|
return nil, fmt.Errorf("error writing purposeId field: %v", err)
|
|
}
|
|
|
|
// Add the entityType (3 for Job) and entityId (jobID)
|
|
if err := w.WriteField("entityType", "3"); err != nil { // 3 = Job
|
|
return nil, fmt.Errorf("error writing entityType field: %v", err)
|
|
}
|
|
|
|
if err := w.WriteField("entityId", jobID); err != nil {
|
|
return nil, fmt.Errorf("error writing entityId field: %v", err)
|
|
}
|
|
|
|
// Ensure we have a file with content to upload
|
|
if len(fileContent) == 0 {
|
|
return nil, fmt.Errorf("no file content provided for upload")
|
|
}
|
|
|
|
// Add a description field with the filename for better identification
|
|
if err := w.WriteField("description", filename); err != nil {
|
|
return nil, fmt.Errorf("error writing description field: %v", err)
|
|
}
|
|
|
|
// Add the file - make sure we use the real filename with extension for content-type detection
|
|
// The API requires the file extension to determine the content type
|
|
if !strings.Contains(filename, ".") {
|
|
return nil, fmt.Errorf("filename must include an extension (e.g. .pdf, .docx) for API content type detection")
|
|
}
|
|
|
|
fw, err := w.CreateFormFile("uploadedFile", filename)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating form file: %v", err)
|
|
}
|
|
|
|
bytesWritten, err := io.Copy(fw, bytes.NewReader(fileContent))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error copying file content: %v", err)
|
|
}
|
|
log.Printf("Wrote %d bytes of file content to the form", bytesWritten)
|
|
|
|
// Close the writer
|
|
if err := w.Close(); err != nil {
|
|
return nil, fmt.Errorf("error closing multipart writer: %v", err)
|
|
}
|
|
|
|
// Create the request
|
|
req, err := http.NewRequest("POST", url, &b)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating request: %v", err)
|
|
}
|
|
|
|
// Set headers
|
|
req.Header.Set("Content-Type", w.FormDataContentType())
|
|
req.Header.Set("Cookie", s.Cookie)
|
|
|
|
// Debug information
|
|
log.Printf("Sending request to: %s", url)
|
|
log.Printf("Content-Type: %s", w.FormDataContentType())
|
|
log.Printf("Request body fields: purposeId=%d, entityType=3, entityId=%s, filename=%s", purposeInt, jobID, filename)
|
|
|
|
// Send the request
|
|
resp, err := s.Client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error sending request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Read the response
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error reading response body: %v", err)
|
|
}
|
|
|
|
// Check for errors
|
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
|
log.Printf("API error response: %s - %s", resp.Status, string(body))
|
|
return nil, fmt.Errorf("API returned error: %s - %s", resp.Status, string(body))
|
|
}
|
|
|
|
// Parse the response
|
|
var result map[string]interface{}
|
|
if err := json.Unmarshal(body, &result); err != nil {
|
|
return nil, fmt.Errorf("error parsing response: %v", err)
|
|
}
|
|
|
|
log.Printf("Successfully uploaded attachment %s to job %s", filename, jobID)
|
|
return result, nil
|
|
}
|
|
|
|
// GetAttachmentInfo gets information about a specific attachment
|
|
func (s *Session) GetAttachmentInfo(attachmentID string) (map[string]interface{}, error) {
|
|
url := fmt.Sprintf("%s/attachment/%s", BaseURL, attachmentID)
|
|
|
|
// Create the request
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating request: %v", err)
|
|
}
|
|
|
|
// Set headers
|
|
req.Header.Set("Cookie", s.Cookie)
|
|
|
|
// Send the request
|
|
resp, err := s.Client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error sending request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Read the response
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error reading response body: %v", err)
|
|
}
|
|
|
|
// Check for errors
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("API returned error: %s - %s", resp.Status, string(body))
|
|
}
|
|
|
|
// Parse the response
|
|
var result map[string]interface{}
|
|
if err := json.Unmarshal(body, &result); err != nil {
|
|
return nil, fmt.Errorf("error parsing response: %v", err)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// DeleteAttachment deletes an attachment
|
|
func (s *Session) DeleteAttachment(attachmentID string) error {
|
|
url := fmt.Sprintf("%s/attachment/%s", BaseURL, attachmentID)
|
|
|
|
// Create the request
|
|
req, err := http.NewRequest("DELETE", url, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating request: %v", err)
|
|
}
|
|
|
|
// Set headers
|
|
req.Header.Set("Cookie", s.Cookie)
|
|
|
|
// Send the request
|
|
resp, err := s.Client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("error sending request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Check for errors
|
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return fmt.Errorf("API returned error: %s - %s", resp.Status, string(body))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetJobAttachments retrieves all attachments for a given job ID
|
|
func (s *Session) GetJobAttachments(jobID string) ([]map[string]interface{}, error) {
|
|
log.Printf("GetJobAttachments: Fetching attachments for job %s", jobID)
|
|
url := fmt.Sprintf("%s/job/%s/paperwork", BaseURL, jobID)
|
|
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating request: %v", err)
|
|
}
|
|
|
|
// Add authorization header
|
|
req.Header.Set("Cookie", s.Cookie)
|
|
log.Printf("GetJobAttachments: Authorization cookie length: %d", len(s.Cookie))
|
|
|
|
// Send the request
|
|
resp, err := s.Client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error sending request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Read the full response body for complete error reporting
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error reading response body: %v", err)
|
|
}
|
|
|
|
// Check for various error responses
|
|
if resp.StatusCode != http.StatusOK {
|
|
// Check for authentication-related errors
|
|
if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
|
|
log.Printf("GetJobAttachments: Authentication error (status %d) for job %s", resp.StatusCode, jobID)
|
|
return nil, fmt.Errorf("authentication error: %s - %s", resp.Status, string(body))
|
|
}
|
|
|
|
// Check for job not found
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
log.Printf("GetJobAttachments: Job %s not found (status 404)", jobID)
|
|
return nil, fmt.Errorf("job not found: %s", jobID)
|
|
}
|
|
|
|
// Generic error
|
|
log.Printf("GetJobAttachments: API error (status %d) for job %s: %s", resp.StatusCode, jobID, string(body))
|
|
return nil, fmt.Errorf("API returned error: %d %s - %s", resp.StatusCode, resp.Status, string(body))
|
|
}
|
|
|
|
// Parse the response - try multiple formats
|
|
var result struct {
|
|
Attachments []map[string]interface{} `json:"objects"`
|
|
}
|
|
|
|
// Try to parse as standard format first
|
|
if err := json.Unmarshal(body, &result); err != nil {
|
|
// If standard parse fails, try parsing as raw map
|
|
log.Printf("GetJobAttachments: Standard JSON parsing failed, trying alternative formats for job %s: %v", jobID, err)
|
|
var mapResult map[string]interface{}
|
|
if err := json.Unmarshal(body, &mapResult); err != nil {
|
|
return nil, fmt.Errorf("error parsing response: %v", err)
|
|
}
|
|
|
|
// Try to extract from various places in the map
|
|
attachments := make([]map[string]interface{}, 0)
|
|
|
|
// Check for objects array
|
|
if objects, ok := mapResult["objects"].([]interface{}); ok {
|
|
log.Printf("GetJobAttachments: Found %d attachments in 'objects' field for job %s", len(objects), jobID)
|
|
for _, obj := range objects {
|
|
if attachment, ok := obj.(map[string]interface{}); ok {
|
|
attachments = append(attachments, attachment)
|
|
}
|
|
}
|
|
return attachments, nil
|
|
}
|
|
|
|
// Check for data.attachments
|
|
if data, ok := mapResult["data"].(map[string]interface{}); ok {
|
|
if attList, ok := data["attachments"].([]interface{}); ok {
|
|
log.Printf("GetJobAttachments: Found %d attachments in 'data.attachments' field for job %s", len(attList), jobID)
|
|
for _, att := range attList {
|
|
if attachment, ok := att.(map[string]interface{}); ok {
|
|
attachments = append(attachments, attachment)
|
|
}
|
|
}
|
|
return attachments, nil
|
|
}
|
|
}
|
|
|
|
// If nothing found, return empty with a warning
|
|
log.Printf("GetJobAttachments: No attachments found in response for job %s: %s", jobID, string(body))
|
|
return attachments, nil
|
|
}
|
|
|
|
if len(result.Attachments) == 0 {
|
|
log.Printf("GetJobAttachments: No attachments found for job %s in standard format", jobID)
|
|
} else {
|
|
log.Printf("GetJobAttachments: Found %d attachments for job %s in standard format", len(result.Attachments), jobID)
|
|
}
|
|
|
|
return result.Attachments, nil
|
|
}
|
|
|
|
// UploadAttachmentFile uploads a file as an attachment to a job using streaming (for large files)
|
|
// Instead of loading the entire file into memory, this method streams the file directly from the reader
|
|
func (s *Session) UploadAttachmentFile(jobID, filename, purpose string, fileReader io.Reader) (map[string]interface{}, error) {
|
|
url := fmt.Sprintf("%s/attachment", BaseURL)
|
|
|
|
// Create a pipe for streaming the multipart data
|
|
pr, pw := io.Pipe()
|
|
|
|
// Create a multipart writer
|
|
writer := multipart.NewWriter(pw)
|
|
|
|
// Start a goroutine to write the multipart form
|
|
go func() {
|
|
defer pw.Close() // Make sure to close the writer to signal end of the form
|
|
|
|
var formError error
|
|
|
|
// Log received values
|
|
log.Printf("Streaming attachment upload to job ID: %s", jobID)
|
|
log.Printf("Filename: %s", filename)
|
|
log.Printf("Purpose value: '%s'", purpose)
|
|
|
|
// The ServiceTrade API expects the purpose ID as an integer
|
|
purposeStr := strings.TrimSpace(purpose)
|
|
|
|
// Try to parse the purpose as an integer, removing any leading zeros first
|
|
purposeStr = strings.TrimLeft(purposeStr, "0")
|
|
if purposeStr == "" {
|
|
purposeStr = "0" // If only zeros were provided
|
|
}
|
|
|
|
purposeInt, err := strconv.Atoi(purposeStr)
|
|
if err != nil {
|
|
formError = fmt.Errorf("invalid purpose value '%s': must be a valid integer: %v", purpose, err)
|
|
pw.CloseWithError(formError)
|
|
return
|
|
}
|
|
|
|
log.Printf("Using purpose value: %d for job: %s", purposeInt, jobID)
|
|
|
|
// Add the purposeId (attachment type) as an integer
|
|
if err := writer.WriteField("purposeId", fmt.Sprintf("%d", purposeInt)); err != nil {
|
|
formError = fmt.Errorf("error writing purposeId field: %v", err)
|
|
pw.CloseWithError(formError)
|
|
return
|
|
}
|
|
|
|
// Add the entityType (3 for Job) and entityId (jobID)
|
|
if err := writer.WriteField("entityType", "3"); err != nil { // 3 = Job
|
|
formError = fmt.Errorf("error writing entityType field: %v", err)
|
|
pw.CloseWithError(formError)
|
|
return
|
|
}
|
|
|
|
if err := writer.WriteField("entityId", jobID); err != nil {
|
|
formError = fmt.Errorf("error writing entityId field: %v", err)
|
|
pw.CloseWithError(formError)
|
|
return
|
|
}
|
|
|
|
// Add a description field with the filename for better identification
|
|
if err := writer.WriteField("description", filename); err != nil {
|
|
formError = fmt.Errorf("error writing description field: %v", err)
|
|
pw.CloseWithError(formError)
|
|
return
|
|
}
|
|
|
|
// Check that filename has an extension
|
|
if !strings.Contains(filename, ".") {
|
|
formError = fmt.Errorf("filename must include an extension (e.g. .pdf, .docx) for API content type detection")
|
|
pw.CloseWithError(formError)
|
|
return
|
|
}
|
|
|
|
// Create the form file field
|
|
part, err := writer.CreateFormFile("uploadedFile", filename)
|
|
if err != nil {
|
|
formError = fmt.Errorf("error creating form file: %v", err)
|
|
pw.CloseWithError(formError)
|
|
return
|
|
}
|
|
|
|
// Stream the file content directly from the reader to the form
|
|
bytesWritten, err := io.Copy(part, fileReader)
|
|
if err != nil {
|
|
formError = fmt.Errorf("error copying file content: %v", err)
|
|
pw.CloseWithError(formError)
|
|
return
|
|
}
|
|
|
|
log.Printf("Streamed %d bytes to multipart form", bytesWritten)
|
|
|
|
// Close the writer to finish the multipart message
|
|
if err := writer.Close(); err != nil {
|
|
formError = fmt.Errorf("error closing multipart writer: %v", err)
|
|
pw.CloseWithError(formError)
|
|
return
|
|
}
|
|
|
|
log.Printf("Multipart form completed successfully")
|
|
}()
|
|
|
|
// Create the request with the pipe reader as the body
|
|
req, err := http.NewRequest("POST", url, pr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating request: %v", err)
|
|
}
|
|
|
|
// Set headers
|
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
req.Header.Set("Cookie", s.Cookie)
|
|
// Don't set Content-Length since we're streaming and don't know the size in advance
|
|
|
|
// Send the request and wait for the response
|
|
resp, err := s.Client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error sending request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Read the response
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error reading response body: %v", err)
|
|
}
|
|
|
|
// Check for errors
|
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
|
log.Printf("API error response: %s - %s", resp.Status, string(body))
|
|
return nil, fmt.Errorf("API returned error: %s - %s", resp.Status, string(body))
|
|
}
|
|
|
|
// Parse the response
|
|
var result map[string]interface{}
|
|
if err := json.Unmarshal(body, &result); err != nil {
|
|
return nil, fmt.Errorf("error parsing response: %v", err)
|
|
}
|
|
|
|
log.Printf("Successfully uploaded streaming attachment %s to job %s", filename, jobID)
|
|
return result, nil
|
|
}
|
|
|
|
// GetJobAttachmentsDirect tries to get attachments directly using the attachment endpoint
|
|
func (s *Session) GetJobAttachmentsDirect(jobID string) ([]map[string]interface{}, error) {
|
|
log.Printf("GetJobAttachmentsDirect: Fetching attachments directly for job %s", jobID)
|
|
|
|
// Try to get attachments using the attachment endpoint with entityId filter
|
|
url := fmt.Sprintf("%s/attachment", BaseURL)
|
|
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating request: %v", err)
|
|
}
|
|
|
|
// Add authorization header
|
|
req.Header.Set("Cookie", s.Cookie)
|
|
|
|
// Add query parameters to filter by job
|
|
q := req.URL.Query()
|
|
q.Add("entityType", "3") // 3 = Job
|
|
q.Add("entityId", jobID)
|
|
req.URL.RawQuery = q.Encode()
|
|
|
|
log.Printf("GetJobAttachmentsDirect: Using URL: %s", req.URL.String())
|
|
|
|
// Send the request
|
|
resp, err := s.Client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error sending request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Read the response
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error reading response body: %v", err)
|
|
}
|
|
|
|
// Check for errors
|
|
if resp.StatusCode != http.StatusOK {
|
|
log.Printf("GetJobAttachmentsDirect: API error (status %d) for job %s: %s", resp.StatusCode, jobID, string(body))
|
|
return nil, fmt.Errorf("API returned error: %d %s - %s", resp.StatusCode, resp.Status, string(body))
|
|
}
|
|
|
|
// Log response preview
|
|
responsePreview := string(body)
|
|
if len(responsePreview) > 200 {
|
|
responsePreview = responsePreview[:200] + "... [truncated]"
|
|
}
|
|
log.Printf("GetJobAttachmentsDirect: Response preview for job %s: %s", jobID, responsePreview)
|
|
|
|
// Parse the response
|
|
var result map[string]interface{}
|
|
if err := json.Unmarshal(body, &result); err != nil {
|
|
return nil, fmt.Errorf("error parsing response: %v", err)
|
|
}
|
|
|
|
// Try to extract attachments from various response structures
|
|
attachments := make([]map[string]interface{}, 0)
|
|
|
|
// Check for data.attachments (this is what the API is actually returning)
|
|
if data, ok := result["data"].(map[string]interface{}); ok {
|
|
if attList, ok := data["attachments"].([]interface{}); ok {
|
|
log.Printf("GetJobAttachmentsDirect: Found %d attachments in data.attachments for job %s", len(attList), jobID)
|
|
for _, att := range attList {
|
|
if attachment, ok := att.(map[string]interface{}); ok {
|
|
attachments = append(attachments, attachment)
|
|
}
|
|
}
|
|
return attachments, nil
|
|
}
|
|
}
|
|
|
|
// Check for objects array at root level
|
|
if objects, ok := result["objects"].([]interface{}); ok {
|
|
log.Printf("GetJobAttachmentsDirect: Found %d objects at root level for job %s", len(objects), jobID)
|
|
for _, obj := range objects {
|
|
if attachment, ok := obj.(map[string]interface{}); ok {
|
|
attachments = append(attachments, attachment)
|
|
}
|
|
}
|
|
return attachments, nil
|
|
}
|
|
|
|
// Check for data.objects
|
|
if data, ok := result["data"].(map[string]interface{}); ok {
|
|
if objects, ok := data["objects"].([]interface{}); ok {
|
|
log.Printf("GetJobAttachmentsDirect: Found %d objects in data.objects for job %s", len(objects), jobID)
|
|
for _, obj := range objects {
|
|
if attachment, ok := obj.(map[string]interface{}); ok {
|
|
attachments = append(attachments, attachment)
|
|
}
|
|
}
|
|
return attachments, nil
|
|
}
|
|
}
|
|
|
|
log.Printf("GetJobAttachmentsDirect: No attachments found for job %s", jobID)
|
|
return attachments, nil
|
|
}
|
|
|
|
// Helper function to check if a string matches any pattern in a slice
|
|
|