├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── 01-sdk-bug.yml │ ├── 02-sdk-feature-request.yml │ ├── 03-blank-issue.md │ └── config.yml ├── dependabot.yml ├── policies │ ├── msgraph-sdk-go-core-branch-protection.yml │ └── resourceManagement.yml ├── pull_request_template.md ├── release-please.yml └── workflows │ ├── auto-merge-dependabot.yml │ ├── codeql-analysis.yml │ ├── go.yml │ ├── project-auto-add.yml │ └── sonarcloud.yml ├── .gitignore ├── .release-please-manifest.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── THIRD PARTY NOTICES ├── authentication ├── auzre_identity_access_token_provider_test.go ├── azure_identity_access_token_provider.go ├── azure_identity_authentication_provider.go └── azure_identity_authentication_provider_test.go ├── batch_item_model.go ├── batch_request_collection.go ├── batch_request_collection_test.go ├── batch_request_test.go ├── batch_requests.go ├── batch_response_model.go ├── docs └── batch_request.md ├── error_mappings_registry.go ├── error_mappings_registry_test.go ├── fileuploader ├── file_uploader_util.go ├── large_file_session.go ├── large_file_upload_task.go ├── large_file_upload_test.go └── upload_slice.go ├── go.mod ├── go.sum ├── graph_client_factory.go ├── graph_client_options.go ├── graph_request_adapter_base.go ├── graph_telemetry_handler.go ├── graph_telemetry_handler_test.go ├── internal ├── errors.go ├── invalid_user_response.go ├── test_byte_stream.go ├── user.go ├── user_delta_response.go └── user_response.go ├── page_iterator.go ├── page_iterator_test.go ├── release-please-config.json ├── sonar-project.properties └── version.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @microsoftgraph/msgraph-devx-go-write 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/01-sdk-bug.yml: -------------------------------------------------------------------------------- 1 | name: SDK Bug Report 2 | description: File SDK bug report 3 | labels: ["type:bug", "status:waiting-for-triage"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | **Thank you for taking the time to fill out this bug report!** 9 | 💥Before submitting a new request, please search existing issues to see if an issue already exists. 10 | - type: textarea 11 | id: description 12 | attributes: 13 | label: Describe the bug 14 | description: | 15 | Provide a description of the actual behavior observed. If applicable please include any error messages, exception stacktraces or a screenshot. 16 | placeholder: I am trying to do [...] but [...] 17 | validations: 18 | required: true 19 | - type: textarea 20 | id: expected-behavior 21 | attributes: 22 | label: Expected behavior 23 | description: | 24 | A clear and concise description of what you expected to happen. 25 | placeholder: Expected behavior 26 | validations: 27 | required: true 28 | - type: textarea 29 | id: repro-steps 30 | attributes: 31 | label: How to reproduce 32 | description: | 33 | Please include minimal steps to reproduce the problem if possible. E.g.: the smallest possible code snippet; or steps to run project in link above. If possible include text as text rather than screenshots (so it shows up in searches). 34 | If there's a link to a public repo where the sample code exists, include it too. 35 | placeholder: Minimal Reproduction steps 36 | validations: 37 | required: true 38 | - type: input 39 | attributes: 40 | label: SDK Version 41 | placeholder: e.g. 5.32.1 42 | description: Version of the SDK with the bug described above. 43 | validations: 44 | required: false 45 | - type: input 46 | id: regression 47 | attributes: 48 | label: Latest version known to work for scenario above? 49 | description: | 50 | Did this work in a previous build or release of the SDK or API client? If you can try a previous release or build to find out, that can help us narrow down the problem. If you don't know, that's OK. 51 | placeholder: version-number 52 | validations: 53 | required: false 54 | - type: textarea 55 | id: known-workarounds 56 | attributes: 57 | label: Known Workarounds 58 | description: | 59 | Please provide a description of any known workarounds. 60 | placeholder: Known Workarounds 61 | validations: 62 | required: false 63 | - type: textarea 64 | id: logs 65 | attributes: 66 | label: Debug output 67 | description: Please copy and paste the debug output below. 68 | value: | 69 |
Click to expand log 70 | ``` 71 | 72 | 73 | 74 | ``` 75 |
76 | validations: 77 | required: false 78 | - type: textarea 79 | id: configuration 80 | attributes: 81 | label: Configuration 82 | description: | 83 | Please provide more information on your SDK configuration: 84 | * What OS and version, and what distro if applicable (Windows 10, Windows 11, MacOS Catalina, Ubuntu 22.04)? 85 | * What is the architecture (x64, x86, ARM, ARM64)? 86 | * Do you know whether it is specific to that configuration? 87 | placeholder: | 88 | - OS: 89 | - architecture: 90 | validations: 91 | required: false 92 | - type: textarea 93 | id: other-info 94 | attributes: 95 | label: Other information 96 | description: | 97 | If you have an idea where the problem might lie, let us know that here. Please include any pointers to code, relevant changes, or related issues you know of. 98 | placeholder: Other information 99 | validations: 100 | required: false 101 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/02-sdk-feature-request.yml: -------------------------------------------------------------------------------- 1 | name: SDK Feature request 2 | description: Request a new feature on the SDK 3 | labels: ["type:feature", "status:waiting-for-triage"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | **Thank you for taking the time to fill out this feature request form!** 9 | 💥Please search to see if an issue already exists for the feature you are requesting. 10 | - type: textarea 11 | attributes: 12 | label: Is your feature request related to a problem? Please describe the problem. 13 | description: A clear and concise description of what the problem is. 14 | placeholder: I am trying to do [...] but [...] 15 | validations: 16 | required: false 17 | - type: textarea 18 | attributes: 19 | label: Describe the solution you'd like. 20 | description: | 21 | A clear and concise description of what you want to happen. Include any alternative solutions you've considered. 22 | validations: 23 | required: true 24 | - type: textarea 25 | attributes: 26 | label: Additional context? 27 | description: | 28 | Add any other context or screenshots about the feature request here. 29 | validations: 30 | required: false 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/03-blank-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Blank issue 3 | about: Something that doesn't fit the other categories 4 | title: '' 5 | labels: ["status:waiting-for-triage"] 6 | assignees: '' 7 | 8 | --- 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Question on use of graph sdk 4 | url: https://github.com/microsoftgraph/msgraph-sdk-go-core/discussions 5 | about: Please add your question in the discussions section of the repo 6 | - name: Question on use of kiota 7 | url: https://github.com/microsoft/kiota/discussions 8 | about: Please add your question in the discussions section of the repo 9 | - name: Question or Feature Request for the MS Graph API? 10 | url: https://aka.ms/msgraphsupport 11 | about: Report an issue or limitation with the MS Graph service APIs 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | labels: 9 | - Go 10 | - package-ecosystem: github-actions 11 | directory: "/" 12 | schedule: 13 | interval: daily 14 | open-pull-requests-limit: 10 -------------------------------------------------------------------------------- /.github/policies/msgraph-sdk-go-core-branch-protection.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | # File initially created using https://github.com/MIchaelMainer/policyservicetoolkit/blob/main/branch_protection_export.ps1. 5 | 6 | name: msgraph-sdk-go-core-branch-protection 7 | description: Branch protection policy for the msgraph-sdk-go-core repository 8 | resource: repository 9 | configuration: 10 | branchProtectionRules: 11 | 12 | - branchNamePattern: main 13 | # This branch pattern applies to the following branches as of 06/12/2023 10:31:15: 14 | # main 15 | 16 | # Specifies whether this branch can be deleted. boolean 17 | allowsDeletions: false 18 | # Specifies whether forced pushes are allowed on this branch. boolean 19 | allowsForcePushes: false 20 | # Specifies whether new commits pushed to the matching branches dismiss pull request review approvals. boolean 21 | dismissStaleReviews: true 22 | # Specifies whether admins can overwrite branch protection. boolean 23 | isAdminEnforced: false 24 | # Indicates whether "Require a pull request before merging" is enabled. boolean 25 | requiresPullRequestBeforeMerging: true 26 | # Specifies the number of pull request reviews before merging. int (0-6). Should be null/empty if PRs are not required 27 | requiredApprovingReviewsCount: 1 28 | # Require review from Code Owners. Requires requiredApprovingReviewsCount. boolean 29 | requireCodeOwnersReview: true 30 | # Are commits required to be signed. boolean. TODO: all contributors must have commit signing on local machines. 31 | requiresCommitSignatures: false 32 | # Are conversations required to be resolved before merging? boolean 33 | requiresConversationResolution: true 34 | # Are merge commits prohibited from being pushed to this branch. boolean 35 | requiresLinearHistory: false 36 | # Required status checks to pass before merging. Values can be any string, but if the value does not correspond to any existing status check, the status check will be stuck on pending for status since nothing exists to push an actual status 37 | requiredStatusChecks: 38 | - build 39 | - SonarCloud 40 | - CodeQL 41 | # Require branches to be up to date before merging. Requires requiredStatusChecks. boolean 42 | requiresStrictStatusChecks: true 43 | # Indicates whether there are restrictions on who can push. boolean. Should be set with whoCanPush. 44 | restrictsPushes: false 45 | # Restrict who can dismiss pull request reviews. boolean 46 | restrictsReviewDismissals: false 47 | 48 | -------------------------------------------------------------------------------- /.github/policies/resourceManagement.yml: -------------------------------------------------------------------------------- 1 | id: 2 | name: GitOps.PullRequestIssueManagement 3 | description: GitOps.PullRequestIssueManagement primitive 4 | owner: 5 | resource: repository 6 | disabled: false 7 | where: 8 | configuration: 9 | resourceManagementConfiguration: 10 | scheduledSearches: 11 | - description: 12 | frequencies: 13 | - hourly: 14 | hour: 6 15 | filters: 16 | - isIssue 17 | - isOpen 18 | - hasLabel: 19 | label: status:waiting-for-author-feedback 20 | - hasLabel: 21 | label: status no recent activity 22 | - noActivitySince: 23 | days: 3 24 | actions: 25 | - closeIssue 26 | - description: 27 | frequencies: 28 | - hourly: 29 | hour: 6 30 | filters: 31 | - isIssue 32 | - isOpen 33 | - hasLabel: 34 | label: status:waiting-for-author-feedback 35 | - noActivitySince: 36 | days: 4 37 | - isNotLabeledWith: 38 | label: status no recent activity 39 | actions: 40 | - addLabel: 41 | label: status no recent activity 42 | - addReply: 43 | reply: This issue has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for **4 days**. It will be closed if no further activity occurs **within 3 days of this comment**. 44 | - description: 45 | frequencies: 46 | - hourly: 47 | hour: 6 48 | filters: 49 | - isIssue 50 | - isOpen 51 | - hasLabel: 52 | label: duplicate 53 | - noActivitySince: 54 | days: 1 55 | actions: 56 | - addReply: 57 | reply: This issue has been marked as duplicate and has not had any activity for **1 day**. It will be closed for housekeeping purposes. 58 | - closeIssue 59 | eventResponderTasks: 60 | - if: 61 | - payloadType: Issues 62 | - isAction: 63 | action: Closed 64 | - hasLabel: 65 | label: 'status:waiting-for-author-feedback' 66 | then: 67 | - removeLabel: 68 | label: 'status:waiting-for-author-feedback' 69 | description: 70 | - if: 71 | - payloadType: Issue_Comment 72 | - isAction: 73 | action: Created 74 | - isActivitySender: 75 | issueAuthor: True 76 | - hasLabel: 77 | label: status:waiting-for-author-feedback 78 | - isOpen 79 | then: 80 | - addLabel: 81 | label: 'needs attention :wave:' 82 | - removeLabel: 83 | label: status:waiting-for-author-feedback 84 | description: 85 | - if: 86 | - payloadType: Issues 87 | - not: 88 | isAction: 89 | action: Closed 90 | - hasLabel: 91 | label: status no recent activity 92 | then: 93 | - removeLabel: 94 | label: status no recent activity 95 | description: 96 | - if: 97 | - payloadType: Issue_Comment 98 | - hasLabel: 99 | label: status no recent activity 100 | then: 101 | - removeLabel: 102 | label: status no recent activity 103 | description: 104 | - if: 105 | - payloadType: Pull_Request 106 | then: 107 | - inPrLabel: 108 | label: 'Status: In PR' 109 | description: 110 | onFailure: 111 | onSuccess: 112 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | Brief description of what this PR does 4 | 5 | ## Demo 6 | Optional. Screenshots, examples, etc. 7 | 8 | ## Notes 9 | Optional. Ancilliary topics, caveats, alternative strategies that didn't work out, anything else. 10 | 11 | ## Testing 12 | * How to test this PR 13 | * Prefer bulleted description 14 | * Start after checking out this branch 15 | * Include any setup required, such as bundling scripts, restarting services, etc. 16 | * Include test case and expected output 17 | -------------------------------------------------------------------------------- /.github/release-please.yml: -------------------------------------------------------------------------------- 1 | manifest: true 2 | primaryBranch: main 3 | handleGHRelease: true -------------------------------------------------------------------------------- /.github/workflows/auto-merge-dependabot.yml: -------------------------------------------------------------------------------- 1 | name: Auto-merge dependabot updates 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | 7 | permissions: 8 | pull-requests: write 9 | contents: write 10 | 11 | jobs: 12 | 13 | dependabot-merge: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | if: ${{ github.actor == 'dependabot[bot]' }} 18 | 19 | steps: 20 | - name: Dependabot metadata 21 | id: metadata 22 | uses: dependabot/fetch-metadata@v2.4.0 23 | with: 24 | github-token: "${{ secrets.GITHUB_TOKEN }}" 25 | 26 | - name: Enable auto-merge for Dependabot PRs 27 | # Only if version bump is not a major version change 28 | if: ${{steps.metadata.outputs.update-type != 'version-update:semver-major'}} 29 | run: gh pr merge --auto --merge "$PR_URL" 30 | env: 31 | PR_URL: ${{github.event.pull_request.html_url}} 32 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 33 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '40 16 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v3 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v3 71 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ main ] 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | env: 13 | relativePath: ./ 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | submodules: recursive 18 | - uses: actions/setup-go@v5 19 | with: 20 | go-version: '^1.24' 21 | - name: Install dependencies 22 | run: go install 23 | working-directory: ${{ env.relativePath }} 24 | - name: Build SDK project 25 | run: go build 26 | working-directory: ${{ env.relativePath }} 27 | - name: Run unit tests 28 | run: go test ./... 29 | working-directory: ${{ env.relativePath }} 30 | -------------------------------------------------------------------------------- /.github/workflows/project-auto-add.yml: -------------------------------------------------------------------------------- 1 | # This workflow is used to add new issues to GitHub GraphSDKs Project 2 | 3 | name: Add Issue or PR to project 4 | on: 5 | issues: 6 | types: 7 | - opened 8 | pull_request: 9 | types: 10 | - opened 11 | branches: 12 | - "main" 13 | 14 | jobs: 15 | track_issue: 16 | if: github.actor != 'dependabot[bot]' && github.event.pull_request.head.repo.fork == false 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Generate token 20 | id: generate_token 21 | uses: actions/create-github-app-token@v2 22 | with: 23 | app-id: ${{ secrets.GRAPHBOT_APP_ID }} 24 | private-key: ${{ secrets.GRAPHBOT_APP_PEM }} 25 | 26 | - name: Get project data 27 | env: 28 | GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} 29 | ORGANIZATION: microsoftgraph 30 | PROJECT_NUMBER: 55 31 | run: | 32 | gh api graphql -f query=' 33 | query($org: String!, $number: Int!) { 34 | organization(login: $org){ 35 | projectV2(number: $number) { 36 | id 37 | fields(first:20) { 38 | nodes { 39 | ... on ProjectV2SingleSelectField { 40 | id 41 | name 42 | options { 43 | id 44 | name 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } 51 | }' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json 52 | 53 | echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV 54 | echo 'LANGUAGE_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Language") | .id' project_data.json) >> $GITHUB_ENV 55 | echo 'LANGUAGE_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Language") | .options[] | select(.name=="Go") |.id' project_data.json) >> $GITHUB_ENV 56 | 57 | - name: Add Issue or PR to project 58 | env: 59 | GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} 60 | ISSUE_ID: ${{ github.event_name == 'issues' && github.event.issue.node_id || github.event.pull_request.node_id }} 61 | run: | 62 | item_id="$( gh api graphql -f query=' 63 | mutation($project:ID!, $issue:ID!) { 64 | addProjectV2ItemById(input: {projectId: $project, contentId: $issue}) { 65 | item { 66 | id 67 | } 68 | } 69 | }' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectV2ItemById.item.id')" 70 | 71 | echo 'ITEM_ID='$item_id >> $GITHUB_ENV 72 | 73 | - name: Set Language 74 | env: 75 | GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} 76 | run: | 77 | gh api graphql -f query=' 78 | mutation ( 79 | $project: ID! 80 | $item: ID! 81 | $language_field: ID! 82 | $language_value: String! 83 | ) { 84 | set_status: updateProjectV2ItemFieldValue(input: { 85 | projectId: $project 86 | itemId: $item 87 | fieldId: $language_field 88 | value: {singleSelectOptionId: $language_value} 89 | }) { 90 | projectV2Item { 91 | id 92 | } 93 | } 94 | }' -f project=$PROJECT_ID -f item=$ITEM_ID -f language_field=$LANGUAGE_FIELD_ID -f language_value=${{ env.LANGUAGE_OPTION_ID }} --silent 95 | -------------------------------------------------------------------------------- /.github/workflows/sonarcloud.yml: -------------------------------------------------------------------------------- 1 | name: SonarCloud 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | types: [opened, synchronize, reopened] 8 | jobs: 9 | sonarcloud: 10 | name: SonarCloud 11 | runs-on: ubuntu-latest 12 | if: ${{ !github.event.pull_request.head.repo.fork }} 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis 17 | submodules: recursive 18 | - uses: actions/setup-go@v5 19 | with: 20 | go-version: '^1.24' 21 | - name: Install dependencies 22 | run: go install 23 | working-directory: ${{ env.relativePath }} 24 | - name: Build SDK project 25 | run: go build 26 | working-directory: ${{ env.relativePath }} 27 | - name: Run unit tests 28 | run: go test -coverprofile cover.out -coverpkg=./... ./... 29 | working-directory: ${{ env.relativePath }} 30 | - name: SonarCloud Scan 31 | uses: SonarSource/sonarqube-scan-action@v5 32 | env: 33 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # Jetbrains files 18 | .idea/ 19 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "1.3.2" 3 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [1.3.2](https://github.com/microsoftgraph/msgraph-sdk-go-core/compare/v1.3.1...v1.3.2) (2025-04-02) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * removes common go dependency ([5431c24](https://github.com/microsoftgraph/msgraph-sdk-go-core/commit/5431c242e5cee8130107c6978aa73d03ba1e08c6)) 11 | * removes common go dependency ([e9d0a32](https://github.com/microsoftgraph/msgraph-sdk-go-core/commit/e9d0a3240562065ec3926841d03e7a29cd98d2fd)) 12 | 13 | ## [1.3.1](https://github.com/microsoftgraph/msgraph-sdk-go-core/compare/v1.3.0...v1.3.1) (2025-03-24) 14 | 15 | 16 | ### Bug Fixes 17 | 18 | * upgrades common go dependency to solve triming issues ([3d78157](https://github.com/microsoftgraph/msgraph-sdk-go-core/commit/3d781577cb8e5776058df106d45a6b8e2731a11f)) 19 | * upgrades common go dependency to solve triming issues ([f731ccc](https://github.com/microsoftgraph/msgraph-sdk-go-core/commit/f731ccc4e96fcfbdc2885f935bae4bf95b9c2900)) 20 | 21 | ## [1.3.0](https://github.com/microsoftgraph/msgraph-sdk-go-core/compare/v1.2.1...v1.3.0) (2025-03-13) 22 | 23 | 24 | ### Features 25 | 26 | * upgrades required go version from go1.18 to go 1.22 ([6a25397](https://github.com/microsoftgraph/msgraph-sdk-go-core/commit/6a25397741b5b20ea899f2a5a4389dc130205168)) 27 | 28 | ## [1.2.1](https://github.com/microsoftgraph/msgraph-sdk-go-core/compare/v1.2.0...v1.2.1) (2024-08-26) 29 | 30 | 31 | ### Bug Fixes 32 | 33 | * repeated slice uploading on large file upload task ([cb329cc](https://github.com/microsoftgraph/msgraph-sdk-go-core/commit/cb329cc395946a619cda5501da88dcda15d84d9b)) 34 | 35 | ## [1.2.0](https://github.com/microsoftgraph/msgraph-sdk-go-core/compare/v1.1.0...v1.2.0) (2024-07-15) 36 | 37 | 38 | ### Features 39 | 40 | * add git release config ([69234a2](https://github.com/microsoftgraph/msgraph-sdk-go-core/commit/69234a236c1d212941e742593ce43d2a35a1212b)) 41 | 42 | 43 | ### Bug Fixes 44 | 45 | * allows registration of page iterator headers ([#309](https://github.com/microsoftgraph/msgraph-sdk-go-core/issues/309)) ([d4b0806](https://github.com/microsoftgraph/msgraph-sdk-go-core/commit/d4b0806dadcc3ccdf07a8eca8ca7b93150094d7f)) 46 | * content range order during upload ([#304](https://github.com/microsoftgraph/msgraph-sdk-go-core/issues/304)) ([f241e94](https://github.com/microsoftgraph/msgraph-sdk-go-core/commit/f241e947b28de38e8f7bc8c3d4eb6eb95b9afbdb)) 47 | 48 | ## [1.1.0](https://github.com/microsoftgraph/msgraph-sdk-go-core/compare/v1.0.2...v1.1.0) (2024-07-10) 49 | 50 | 51 | ### Features 52 | 53 | * add git release config ([69234a2](https://github.com/microsoftgraph/msgraph-sdk-go-core/commit/69234a236c1d212941e742593ce43d2a35a1212b)) 54 | 55 | 56 | ### Bug Fixes 57 | 58 | * content range order during upload ([#304](https://github.com/microsoftgraph/msgraph-sdk-go-core/issues/304)) ([f241e94](https://github.com/microsoftgraph/msgraph-sdk-go-core/commit/f241e947b28de38e8f7bc8c3d4eb6eb95b9afbdb)) 59 | 60 | ## [1.1.0] - 2024-02-02 61 | 62 | ### Added 63 | 64 | - Added support for large file uploads. 65 | 66 | ## [1.0.2] - 2023-12-01 67 | 68 | ### Changed 69 | 70 | - Fixed a bug where GetBatchResponseById failed to deserialize error response bodies. 71 | 72 | ## [1.0.1] - 2023-11-24 73 | 74 | ### Changed 75 | 76 | - Fixed a bug where page iterator would panic if it couldn't find the GetValue method on the collection. 77 | 78 | ## [1.0.0] - 2023-05-04 79 | 80 | ### Changed 81 | 82 | - GA Release. 83 | 84 | ## [0.36.2] - 2023-05-01 85 | 86 | ### Added 87 | 88 | - `PageIterator` exposes `odata.nextLink` and `odata.deltaLink` of most recent page. 89 | 90 | ## [0.36.1] - 2023-04-17 91 | 92 | ### Added 93 | 94 | - Adds url token replacement to batch requests. 95 | 96 | ## [0.36.0] - 2023-03-27 97 | 98 | ### Added 99 | 100 | - Adds `BatchRequestCollection` support. 101 | 102 | ## [0.35.0] - 2023-03-23 103 | 104 | ### Added 105 | 106 | - `PageIterator` uses generics to define return type. 107 | 108 | ## [0.34.1] - 2023-03-06 109 | 110 | ### Changed 111 | 112 | - Change `PageIterator` to use `GetValue` method instead of `value` field to access response. 113 | 114 | ## [0.34.0] - 2023-02-23 115 | 116 | ### Added 117 | 118 | - Adds `UrlReplaceHandler` to default middleware. 119 | 120 | ## [0.33.1] - 2023-01-26 121 | 122 | ### Added 123 | 124 | - Upgrade dependencies to support backing store. 125 | 126 | ## [0.33.0] - 2023-01-17 127 | 128 | ### Added 129 | 130 | - Added authentication provider with Microsoft Graph defaults. 131 | 132 | ## [0.32.0] - 2023-01-11 133 | 134 | ### Changed 135 | 136 | - Upgraded abstractions and http dependencies. 137 | 138 | ## [0.31.1] - 2022-12-15 139 | 140 | ### Changed 141 | 142 | - Fixes path parameters missing when sending batch requests. 143 | - Fixes appending items when sending batch requests. 144 | - Fixes `Send` url when sending batch requests 145 | 146 | ## [0.31.0] - 2022-12-13 147 | 148 | ### Changed 149 | 150 | - Updated references to core libraries for multi-valued request headers. 151 | 152 | ## [0.30.1] - 2022-10-21 153 | 154 | ### Changed 155 | 156 | - Fix: Remove error swallowing in page iterator `fetchNextPage`. 157 | 158 | ## [0.30.0] - 2022-09-29 159 | 160 | ### Added 161 | 162 | - Adds ability to batch requests. 163 | - Adds tracing support via Open Telemetry. 164 | 165 | ## [0.29.0] - 2022-09-27 166 | 167 | ### Changed 168 | 169 | - Updated dependencies for additional serialization methods. 170 | 171 | ## [0.28.1] - 2022-09-09 172 | 173 | ### Changed 174 | 175 | - Updates references to kiota packages. 176 | 177 | ## [0.28.0] - 2022-08-24 178 | 179 | ### Changed 180 | 181 | - Upgrade to library `kiota-abstraction` breaking change 182 | - Introduces `context.Context` object to Page Iterator 183 | 184 | ## [0.27.0] - 2022-07-21 185 | 186 | ### Changed 187 | 188 | - Fixes PageIterator to use updated nextLink property 189 | 190 | ### Changed 191 | 192 | ## [0.26.2] - 2022-06-12 193 | 194 | ### Changed 195 | 196 | - Updated reference to kiota serialization json 197 | - Updated reference to kiota http 198 | 199 | ## [0.26.1] - 2022-06-07 200 | 201 | ### Changed 202 | 203 | - Updated references to kiota libraries and yaml dependencies. 204 | 205 | ## [0.26.0] - 2022-05-27 206 | 207 | ### Changed 208 | 209 | - Updated references to kiota libraries to add support for enum and enum collections responses. 210 | 211 | ## [0.25.1] - 2022-05-25 212 | 213 | ### Changed 214 | 215 | - Updated kiota http library reference. 216 | 217 | ## [0.25.0] - 2022-05-19 218 | 219 | ### Changed 220 | 221 | - Upgraded kiota dependencies for preliminary continuous access evaluation support. 222 | 223 | ## [0.24.0] - 2022-04-28 224 | 225 | ### Changed 226 | 227 | - Updated references to kiota libraries for request configuration revamp 228 | 229 | ## [0.23.0] - 2022-04-19 230 | 231 | ### Changed 232 | 233 | - Upgraded kiota libraries to address quote in url template issue. 234 | - Upgraded to go 18. 235 | 236 | ## [0.22.1] - 2022-04-14 237 | 238 | ### Changed 239 | 240 | - Fixed an issue with date serialization in JSON. 241 | 242 | ## [0.22.0] - 2022-04-12 243 | 244 | ### Changed 245 | 246 | - Updated references to kiota libraries for special character in parameter names support. 247 | - Breaking: removed the odata parameter names handler. 248 | 249 | ## [0.21.0] - 2022-04-06 250 | 251 | ### Changed 252 | 253 | - Updated reference to kiota libraries for deserialization simplification. 254 | 255 | ## [0.20.0] - 2022-03-31 256 | 257 | ### Changed 258 | 259 | - Updated reference to kiota libraries that were moved to their own repository. 260 | 261 | ## [0.0.17] - 2022-03-30 262 | 263 | ### Added 264 | 265 | - Added support for vendor specific content types 266 | - Added support for 204 no content responses 267 | 268 | ### Changed 269 | 270 | - Updated kiota libraries reference. 271 | 272 | ## [0.0.16] - 2022-03-21 273 | 274 | ### Changed 275 | 276 | - Breaking: updates PageIterator to receive a RequestAdapter interface instead of GraphRequestAdapterBase concrete type 277 | - Breaking: removed IsNil method from models 278 | 279 | ## [0.0.15] - 2022-03-15 280 | 281 | ### Changed 282 | 283 | - Updated references to kiota libraries for new supported types (byte, unit8, ...) 284 | 285 | ## [0.0.14] - 2022-03-11 286 | 287 | ### Changed 288 | 289 | - Publishes a version retraction for v0.11.0 that was wrongfully published and causes issues during upgrades 290 | 291 | ## [0.0.13] - 2022-03-04 292 | 293 | ### Changed 294 | 295 | - Breaking: updates kiota dependencies for parsable interface split. 296 | 297 | ## [0.0.12] - 2022-03-03 298 | 299 | ### Changed 300 | 301 | - Breaking: updates kiota dependencies to pass request information by reference and not by copy (request adapter, authentication provider). 302 | 303 | ## [0.0.11] - 2022-03-02 304 | 305 | ### Changed 306 | 307 | - Breaking: updates kiota dependencies references to prepare for type discriminator support. 308 | 309 | ## [0.0.10] - 2022-02-28 310 | 311 | ### Changed 312 | 313 | - Fixed a bug where http client configuration would impact the default client configuration for other usages. 314 | 315 | ## [0.0.9] - 2022-02-16 316 | 317 | ### Added 318 | 319 | - Added support for deserializing error responses (will return error) 320 | 321 | ### Changed 322 | 323 | - Fixed a bug where response body compression would send empty bodies 324 | 325 | ## [0.0.8] - 2022-02-08 326 | 327 | ### Added 328 | 329 | - Added support for request body compression (gzip) 330 | - Added support for response body decompression (gzip) 331 | 332 | ### Changed 333 | 334 | - Fixes a bug where resuming the page iterator wouldn't work 335 | - Fixes a bug where OData query parameters would be added twice in some cases 336 | 337 | ## [0.0.7] - 2022-02-03 338 | 339 | ### Changed 340 | 341 | - Updated references to Kiota packages to fix a [bug where the access token would never be attached to the request](https://github.com/microsoft/kiota/pull/1116). 342 | 343 | ## [0.0.6] - 2022-02-02 344 | 345 | ### Added 346 | 347 | - Adds missing delta token for OData query parameters dollar sign injection. 348 | - Adds PageIterator task 349 | 350 | ## [0.0.5] - 2021-12-02 351 | 352 | ### Changed 353 | 354 | - Fixes a bug where the middleware pipeline would run only on the first request of the client/adapter/http client. 355 | 356 | ## [0.0.4] - 2021-12-01 357 | 358 | ### Changed 359 | 360 | - Adds the missing github.com/microsoft/kiota/authentication/go/azure dependency 361 | 362 | ## [0.0.3] - 2021-11-30 363 | 364 | ### Changed 365 | 366 | - Updated dependencies and switched to Go 17. 367 | 368 | ## [0.0.2] - 2021-11-08 369 | 370 | ### Changed 371 | 372 | - Updated kiota abstractions and http to provide support for setting the base URL 373 | 374 | ## [0.0.1] - 2021-10-22 375 | 376 | ### Added 377 | 378 | - Initial release 379 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | - Employees can reach out at [aka.ms/opensource/moderation-support](https://aka.ms/opensource/moderation-support) 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the Microsoft Graph Core SDK for Go 2 | 3 | The Microsoft Graph Core SDK for Go is available for all manner of contribution. There are a couple of different recommended paths to get contributions into the released version of this SDK. 4 | 5 | __NOTE__ A signed a contribution license agreement is required for all contributions, and is checked automatically on new pull requests. Please read and sign [the agreement](https://cla.microsoft.com/) before starting any work for this repository. 6 | 7 | ## File issues 8 | 9 | The best way to get started with a contribution is to start a dialog with the owners of this repository. Sometimes features will be under development or out of scope for this SDK and it's best to check before starting work on contribution. 10 | 11 | ## Submit pull requests for trivial changes 12 | 13 | If you are making a change that does not affect the interface components and does not affect other downstream callers, feel free to make a pull request against the __dev__ branch. The dev branch will be updated frequently. 14 | 15 | Revisions of this nature will result in a 0.0.X change of the version number. 16 | 17 | ## Submit pull requests for features 18 | 19 | If major functionality is being added, or there will need to be gestation time for a change, it should be submitted against the __feature__ branch. 20 | 21 | Revisions of this nature will result in a 0.X.X change of the version number. 22 | 23 | ## Commit message format 24 | 25 | To support our automated release process, pull requests are required to follow the [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/) 26 | format. 27 | 28 | Each commit message consists of a **header**, an optional **body** and an optional **footer**. The header is the first line of the commit and 29 | MUST have a **type** (see below for a list of types) and a **description**. An optional **scope** can be added to the header to give extra context. 30 | 31 | ``` 32 | [optional scope]: 33 | 34 | 35 | 36 | 37 | ``` 38 | 39 | The recommended commit types used are: 40 | 41 | - **feat** for feature updates (increments the _minor_ version) 42 | - **fix** for bug fixes (increments the _patch_ version) 43 | - **perf** for performance related changes e.g. optimizing an algorithm 44 | - **refactor** for code refactoring changes 45 | - **test** for test suite updates e.g. adding a test or fixing a test 46 | - **style** for changes that don't affect the meaning of code. e.g. formatting changes 47 | - **docs** for documentation updates e.g. ReadMe update or code documentation updates 48 | - **build** for build system changes (gradle updates, external dependency updates) 49 | - **ci** for CI configuration file changes e.g. updating a pipeline 50 | - **chore** for miscallaneous non-sdk changesin the repo e.g. removing an unused file 51 | 52 | Adding a footer with the prefix **BREAKING CHANGE:** will cause an increment of the _major_ version. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Microsoft Graph 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Microsoft Graph Core SDK for Go 2 | 3 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/microsoftgraph/msgraph-sdk-go-core/)](https://pkg.go.dev/github.com/microsoftgraph/msgraph-sdk-go-core/) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=microsoftgraph_msgraph-sdk-go-core&metric=coverage)](https://sonarcloud.io/dashboard?id=microsoftgraph_msgraph-sdk-go-core) [![Sonarcloud Status](https://sonarcloud.io/api/project_badges/measure?project=microsoftgraph_msgraph-sdk-go-core&metric=alert_status)](https://sonarcloud.io/dashboard?id=microsoftgraph_msgraph-sdk-go-core) 4 | 5 | Get started with the Microsoft Graph Core SDK for Go by integrating the [Microsoft Graph API](https://docs.microsoft.com/graph/overview) into your Go application! You can also have a look at the [Go documentation](https://pkg.go.dev/github.com/microsoftgraph/msgraph-sdk-go-core/) 6 | 7 | > **Note:** Although you can use this library directly, we recommend you use the [v1](https://github.com/microsoftgraph/msgraph-sdk-go) or [beta](https://github.com/microsoftgraph/msgraph-beta-sdk-go) library which rely on this library and additionally provide a fluent style Go API and models. 8 | > 9 | > **Note:** The Microsoft Graph Go SDK is currently in Release Candidate (RC) version starting from version 0.34.1. The SDK is still undergoing testing but minimum breaking changes should be expected. Checkout the [known limitations](https://github.com/microsoftgraph/msgraph-sdk-go-core/issues/1). 10 | 11 | ## Samples and usage guide 12 | 13 | - [Middleware usage](https://github.com/microsoftgraph/msgraph-sdk-design/) 14 | 15 | ## 1. Installation 16 | 17 | ```Shell 18 | go get github.com/microsoftgraph/msgraph-sdk-go-core 19 | go get github.com/Azure/azure-sdk-for-go/sdk/azidentity 20 | ``` 21 | 22 | ## 2. Getting started 23 | 24 | ### 2.1 Register your application 25 | 26 | Register your application by following the steps at [Register your app with the Microsoft Identity Platform](https://docs.microsoft.com/graph/auth-register-app-v2). 27 | 28 | ### 2.2 Create an AuthenticationProvider object 29 | 30 | An instance of the **GraphRequestAdapterBase** class handles building client. To create a new instance of this class, you need to provide an instance of **AuthenticationProvider**, which can authenticate requests to Microsoft Graph. 31 | 32 | For an example of how to get an authentication provider, see [choose a Microsoft Graph authentication provider](https://docs.microsoft.com/graph/sdks/choose-authentication-providers?tabs=Go). 33 | 34 | > Note: we are working to add the getting started information for Go to our public documentation, in the meantime the following sample should help you getting started. 35 | 36 | ```Golang 37 | import ( 38 | azidentity "github.com/Azure/azure-sdk-for-go/sdk/azidentity" 39 | a "github.com/microsoftgraph/msgraph-sdk-go-core/authentication" 40 | "context" 41 | ) 42 | 43 | cred, err := azidentity.NewDeviceCodeCredential(&azidentity.DeviceCodeCredentialOptions{ 44 | TenantID: "", 45 | ClientID: "", 46 | UserPrompt: func(ctx context.Context, message azidentity.DeviceCodeMessage) error { 47 | fmt.Println(message.Message) 48 | return nil 49 | }, 50 | }) 51 | 52 | if err != nil { 53 | fmt.Printf("Error creating credentials: %v\n", err) 54 | } 55 | 56 | auth, err := a.NewAzureIdentityAuthenticationProviderWithScopes(cred, []string{"Mail.Read", "Mail.Send"}) 57 | if err != nil { 58 | fmt.Printf("Error authentication provider: %v\n", err) 59 | return 60 | } 61 | 62 | ``` 63 | 64 | ### 2.3 Get a Request Adapter object 65 | 66 | You must get a **GraphRequestAdapterBase** object to make requests against the service. 67 | 68 | ```Golang 69 | import core "github.com/microsoftgraph/msgraph-sdk-go-core" 70 | 71 | adapter, err := core.NewGraphRequestAdapterBase(auth) 72 | if err != nil { 73 | fmt.Printf("Error creating adapter: %v\n", err) 74 | return 75 | } 76 | ``` 77 | 78 | ## 3. Make requests against the service 79 | 80 | After you have a **GraphRequestAdapterBase** that is authenticated, you can begin making calls against the service. The requests against the service look like our [REST API](https://docs.microsoft.com/graph/api/overview?view=graph-rest-1.0). 81 | 82 | ### 3.1 Get the user's details 83 | 84 | To retrieve the user's details 85 | 86 | ```Golang 87 | import abs "github.com/microsoft/kiota-abstractions-go" 88 | 89 | requestInf := abs.NewRequestInformation() 90 | targetUrl, err := url.Parse("https://graph.microsoft.com/v1.0/me") 91 | if err != nil { 92 | fmt.Printf("Error parsing URL: %v\n", err) 93 | } 94 | requestInf.SetUri(*targetUrl) 95 | 96 | // User is your own type that implements Parsable or comes from the service library 97 | user, err := adapter.SendAsync(*requestInf, func() { return &User }, nil) 98 | 99 | if err != nil { 100 | fmt.Printf("Error getting the user: %v\n", err) 101 | } 102 | ``` 103 | 104 | ## 4. Issues 105 | 106 | For known issues, see [issues](https://github.com/MicrosoftGraph/msgraph-sdk-go-core/issues). 107 | 108 | ## 5. Contributions 109 | 110 | The Microsoft Graph SDK is open for contribution. To contribute to this project, see [Contributing](https://github.com/microsoftgraph/msgraph-sdk-go-core/blob/main/CONTRIBUTING.md). 111 | 112 | ## 6. License 113 | 114 | Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the [MIT license](LICENSE). 115 | 116 | ## 7. Third-party notices 117 | 118 | [Third-party notices](THIRD%20PARTY%20NOTICES) 119 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /THIRD PARTY NOTICES: -------------------------------------------------------------------------------- 1 | This file is based on or incorporates material from the projects listed below 2 | (Third Party IP). The original copyright notice and the license under which 3 | Microsoft received such Third Party IP, are set forth below. Such licenses and 4 | notices are provided for informational purposes only. Microsoft licenses the 5 | Third Party IP to you under the licensing terms for the Microsoft product. 6 | Microsoft reserves all other rights not expressly granted under this agreement, 7 | whether by implication, estoppel or otherwise. 8 | 9 | -------------------------------------------------------------------------------- /authentication/auzre_identity_access_token_provider_test.go: -------------------------------------------------------------------------------- 1 | package authentication 2 | 3 | import ( 4 | "testing" 5 | 6 | absauth "github.com/microsoft/kiota-abstractions-go/authentication" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestAccessTokenProviderImplementsInterface(t *testing.T) { 11 | var value absauth.AccessTokenProvider = &AzureIdentityAccessTokenProvider{} 12 | assert.NotNil(t, value) 13 | } 14 | -------------------------------------------------------------------------------- /authentication/azure_identity_access_token_provider.go: -------------------------------------------------------------------------------- 1 | package authentication 2 | 3 | import ( 4 | azcore "github.com/Azure/azure-sdk-for-go/sdk/azcore" 5 | kiotaidentity "github.com/microsoft/kiota-authentication-azure-go" 6 | ) 7 | 8 | // AzureIdentityAccessTokenProvider is a wrapper around the AzureIdentityAccessTokenProvider from the Kiota library with Microsoft Graph default valid hosts. 9 | type AzureIdentityAccessTokenProvider struct { 10 | kiotaidentity.AzureIdentityAccessTokenProvider 11 | } 12 | 13 | // NewAzureIdentityAccessTokenProvider creates a new instance of the AzureIdentityAccessTokenProvider using ":///.default" as the default scope. 14 | func NewAzureIdentityAccessTokenProvider(credential azcore.TokenCredential) (*AzureIdentityAccessTokenProvider, error) { 15 | return NewAzureIdentityAccessTokenProviderWithScopes(credential, nil) 16 | } 17 | 18 | // NewAzureIdentityAccessTokenProviderWithScopes creates a new instance of the AzureIdentityAccessTokenProvider. 19 | func NewAzureIdentityAccessTokenProviderWithScopes(credential azcore.TokenCredential, scopes []string) (*AzureIdentityAccessTokenProvider, error) { 20 | return NewAzureIdentityAccessTokenProviderWithScopesAndValidHosts(credential, scopes, nil) 21 | } 22 | 23 | // NewAzureIdentityAccessTokenProviderWithScopesAndValidHosts creates a new instance of the AzureIdentityAccessTokenProvider. 24 | func NewAzureIdentityAccessTokenProviderWithScopesAndValidHosts(credential azcore.TokenCredential, scopes []string, validHosts []string) (*AzureIdentityAccessTokenProvider, error) { 25 | return NewAzureIdentityAccessTokenProviderWithScopesAndValidHostsAndObservabilityOptions(credential, scopes, validHosts, kiotaidentity.ObservabilityOptions{}) 26 | } 27 | 28 | // NewAzureIdentityAccessTokenProviderWithScopesAndValidHosts creates a new instance of the AzureIdentityAccessTokenProvider. 29 | func NewAzureIdentityAccessTokenProviderWithScopesAndValidHostsAndObservabilityOptions(credential azcore.TokenCredential, scopes []string, validHosts []string, observabilityOptions kiotaidentity.ObservabilityOptions) (*AzureIdentityAccessTokenProvider, error) { 30 | base, err := kiotaidentity.NewAzureIdentityAccessTokenProviderWithScopesAndValidHostsAndObservabilityOptions(credential, scopes, validHosts, observabilityOptions) 31 | if err != nil { 32 | return nil, err 33 | } 34 | if len(validHosts) == 0 { 35 | base.GetAllowedHostsValidator().SetAllowedHosts([]string{"graph.microsoft.com", "graph.microsoft.us", "dod-graph.microsoft.us", "graph.microsoft.de", "microsoftgraph.chinacloudapi.cn", "canary.graph.microsoft.com"}) 36 | } 37 | result := &AzureIdentityAccessTokenProvider{ 38 | AzureIdentityAccessTokenProvider: *base, 39 | } 40 | 41 | return result, nil 42 | } 43 | -------------------------------------------------------------------------------- /authentication/azure_identity_authentication_provider.go: -------------------------------------------------------------------------------- 1 | package authentication 2 | 3 | import ( 4 | azcore "github.com/Azure/azure-sdk-for-go/sdk/azcore" 5 | absauth "github.com/microsoft/kiota-abstractions-go/authentication" 6 | kiotaidentity "github.com/microsoft/kiota-authentication-azure-go" 7 | ) 8 | 9 | // AzureIdentityAuthenticationProvider is a wrapper around the AzureIdentityAuthenticationProvider that sets default values for Microsoft Graph. 10 | type AzureIdentityAuthenticationProvider struct { 11 | kiotaidentity.AzureIdentityAuthenticationProvider 12 | } 13 | 14 | // NewAzureIdentityAuthenticationProvider creates a new instance of the AzureIdentityAuthenticationProvider using "https://graph.microsoft.com/.default" as the default scope. 15 | func NewAzureIdentityAuthenticationProvider(credential azcore.TokenCredential) (*AzureIdentityAuthenticationProvider, error) { 16 | return NewAzureIdentityAuthenticationProviderWithScopes(credential, nil) 17 | } 18 | 19 | // NewAzureIdentityAuthenticationProviderWithScopes creates a new instance of the AzureIdentityAuthenticationProvider. 20 | func NewAzureIdentityAuthenticationProviderWithScopes(credential azcore.TokenCredential, scopes []string) (*AzureIdentityAuthenticationProvider, error) { 21 | return NewAzureIdentityAuthenticationProviderWithScopesAndValidHosts(credential, scopes, nil) 22 | } 23 | 24 | // NewAzureIdentityAuthenticationProviderWithScopesAndValidHosts creates a new instance of the AzureIdentityAuthenticationProvider. 25 | func NewAzureIdentityAuthenticationProviderWithScopesAndValidHosts(credential azcore.TokenCredential, scopes []string, validHosts []string) (*AzureIdentityAuthenticationProvider, error) { 26 | return NewAzureIdentityAuthenticationProviderWithScopesAndValidHostsAndObservabilityOptions(credential, scopes, validHosts, kiotaidentity.ObservabilityOptions{}) 27 | } 28 | 29 | // NewAzureIdentityAuthenticationProviderWithScopesAndValidHostsAndObservabilityOptions creates a new instance of the AzureIdentityAuthenticationProvider. 30 | func NewAzureIdentityAuthenticationProviderWithScopesAndValidHostsAndObservabilityOptions(credential azcore.TokenCredential, scopes []string, validHosts []string, observabilityOptions kiotaidentity.ObservabilityOptions) (*AzureIdentityAuthenticationProvider, error) { 31 | accessTokenProvider, err := NewAzureIdentityAccessTokenProviderWithScopesAndValidHostsAndObservabilityOptions(credential, scopes, validHosts, observabilityOptions) 32 | if err != nil { 33 | return nil, err 34 | } 35 | baseBearer := absauth.NewBaseBearerTokenAuthenticationProvider(accessTokenProvider) 36 | result := &AzureIdentityAuthenticationProvider{ 37 | AzureIdentityAuthenticationProvider: kiotaidentity.AzureIdentityAuthenticationProvider{ 38 | BaseBearerTokenAuthenticationProvider: *baseBearer, 39 | }, 40 | } 41 | return result, nil 42 | } 43 | -------------------------------------------------------------------------------- /authentication/azure_identity_authentication_provider_test.go: -------------------------------------------------------------------------------- 1 | package authentication 2 | 3 | import ( 4 | "testing" 5 | 6 | absauth "github.com/microsoft/kiota-abstractions-go/authentication" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestAuthenticationProviderImplementsInterface(t *testing.T) { 11 | var value absauth.AuthenticationProvider = &AzureIdentityAuthenticationProvider{} 12 | assert.NotNil(t, value) 13 | } 14 | -------------------------------------------------------------------------------- /batch_item_model.go: -------------------------------------------------------------------------------- 1 | package msgraphgocore 2 | 3 | import ( 4 | "errors" 5 | abs "github.com/microsoft/kiota-abstractions-go" 6 | "github.com/microsoft/kiota-abstractions-go/serialization" 7 | jsonserialization "github.com/microsoft/kiota-serialization-json-go" 8 | "reflect" 9 | ) 10 | 11 | // BatchItem is an instance of the BatchRequest payload to be later serialized to a json payload 12 | type BatchItem interface { 13 | serialization.Parsable 14 | GetId() *string 15 | SetId(value *string) 16 | GetMethod() *string 17 | SetMethod(value *string) 18 | GetUrl() *string 19 | SetUrl(value *string) 20 | GetHeaders() RequestHeader 21 | SetHeaders(value RequestHeader) 22 | GetBody() RequestBody 23 | SetBody(value RequestBody) 24 | GetDependsOn() []string 25 | SetDependsOn(value []string) 26 | GetStatus() *int32 27 | SetStatus(value *int32) 28 | DependsOnItem(item BatchItem) 29 | } 30 | 31 | type batchItem struct { 32 | Id *string 33 | method *string 34 | Url *string 35 | Headers RequestHeader 36 | Body RequestBody 37 | DependsOn []string 38 | Status *int32 39 | } 40 | 41 | // DependsOnItem creates a dependency chain between BatchItems.If A depends on B, then B will be sent before B 42 | // A batchItem can only depend on one other batchItem 43 | // see: https://docs.microsoft.com/en-us/graph/known-issues#request-dependencies-are-limited 44 | func (bi *batchItem) DependsOnItem(item BatchItem) { 45 | dependsOn := append(item.GetDependsOn(), *item.GetId()) 46 | bi.SetDependsOn(dependsOn) 47 | } 48 | 49 | // NewBatchItem creates an instance of BatchItem 50 | func NewBatchItem() BatchItem { 51 | return &batchItem{ 52 | DependsOn: make([]string, 0), 53 | } 54 | } 55 | 56 | // GetId returns batch item `id` property 57 | func (bi *batchItem) GetId() *string { 58 | return bi.Id 59 | } 60 | 61 | // SetId sets string value as batch item `id` property 62 | func (bi *batchItem) SetId(value *string) { 63 | bi.Id = value 64 | } 65 | 66 | // GetMethod returns batch item `Method` property 67 | func (bi *batchItem) GetMethod() *string { 68 | return bi.method 69 | } 70 | 71 | // SetMethod sets string value as batch item `Method` property 72 | func (bi *batchItem) SetMethod(value *string) { 73 | bi.method = value 74 | } 75 | 76 | // GetUrl returns batch item `Url` property 77 | func (bi *batchItem) GetUrl() *string { 78 | return bi.Url 79 | } 80 | 81 | // SetUrl sets string value as batch item `Url` property 82 | func (bi *batchItem) SetUrl(value *string) { 83 | bi.Url = value 84 | } 85 | 86 | // GetHeaders returns batch item `Header` as a map[string]string 87 | func (bi *batchItem) GetHeaders() RequestHeader { 88 | return bi.Headers 89 | } 90 | 91 | // SetHeaders sets map[string]string value as batch item `Header` property 92 | func (bi *batchItem) SetHeaders(value RequestHeader) { 93 | bi.Headers = value 94 | } 95 | 96 | // GetBody returns batch item `RequestBody` property 97 | func (bi *batchItem) GetBody() RequestBody { 98 | return bi.Body 99 | } 100 | 101 | // SetBody sets map[string]string value as batch item `RequestBody` property 102 | func (bi *batchItem) SetBody(value RequestBody) { 103 | bi.Body = value 104 | } 105 | 106 | // GetDependsOn returns batch item `dependsOn` property as a string array 107 | func (bi *batchItem) GetDependsOn() []string { 108 | return bi.DependsOn 109 | } 110 | 111 | // SetDependsOn sets []string value as batch item `dependsOn` property 112 | func (bi *batchItem) SetDependsOn(value []string) { 113 | bi.DependsOn = value 114 | } 115 | 116 | // GetStatus returns batch item `status` property 117 | func (bi *batchItem) GetStatus() *int32 { 118 | return bi.Status 119 | } 120 | 121 | // SetStatus sets int32 value as batch item `int` property 122 | func (bi *batchItem) SetStatus(value *int32) { 123 | bi.Status = value 124 | } 125 | 126 | // Serialize serializes information the current object 127 | func (bi *batchItem) Serialize(writer serialization.SerializationWriter) error { 128 | { 129 | err := writer.WriteStringValue("id", bi.GetId()) 130 | if err != nil { 131 | return err 132 | } 133 | } 134 | { 135 | err := writer.WriteStringValue("method", bi.GetMethod()) 136 | if err != nil { 137 | return err 138 | } 139 | } 140 | { 141 | err := writer.WriteStringValue("url", bi.GetUrl()) 142 | if err != nil { 143 | return err 144 | } 145 | } 146 | { 147 | err := writer.WriteAnyValue("headers", bi.GetHeaders()) 148 | if err != nil { 149 | return err 150 | } 151 | } 152 | { 153 | err := writer.WriteAnyValue("body", bi.GetBody()) 154 | if err != nil { 155 | return err 156 | } 157 | } 158 | { 159 | err := writer.WriteCollectionOfStringValues("dependsOn", bi.GetDependsOn()) 160 | if err != nil { 161 | return err 162 | } 163 | } 164 | { 165 | err := writer.WriteInt32Value("status", bi.GetStatus()) 166 | if err != nil { 167 | return err 168 | } 169 | } 170 | return nil 171 | } 172 | 173 | // GetFieldDeserializers the deserialization information for the current model 174 | func (bi *batchItem) GetFieldDeserializers() map[string]func(serialization.ParseNode) error { 175 | res := make(map[string]func(serialization.ParseNode) error) 176 | res["id"] = abs.SetStringValue(bi.SetId) 177 | res["method"] = abs.SetStringValue(bi.SetMethod) 178 | res["url"] = abs.SetStringValue(bi.SetUrl) 179 | res["headers"] = func(n serialization.ParseNode) error { 180 | rawVal, err := n.GetRawValue() 181 | if err != nil { 182 | return err 183 | } 184 | 185 | if rawVal == nil { 186 | return nil 187 | } 188 | 189 | result, err := castMapOfStrings(rawVal) 190 | if err != nil { 191 | return err 192 | } 193 | 194 | bi.SetHeaders(result) 195 | return nil 196 | } 197 | res["body"] = func(n serialization.ParseNode) error { 198 | rawVal, err := n.GetRawValue() 199 | if err != nil { 200 | return err 201 | } 202 | 203 | if rawVal == nil { 204 | return nil 205 | } 206 | 207 | result, err := convertToMap(rawVal) 208 | if err != nil { 209 | return err 210 | } 211 | 212 | bi.SetBody(result) 213 | return nil 214 | } 215 | res["dependsOn"] = abs.SetCollectionOfPrimitiveValues("string", bi.SetDependsOn) 216 | res["status"] = abs.SetInt32Value(bi.SetStatus) 217 | return res 218 | } 219 | 220 | func convertToMap(rawVal interface{}) (map[string]interface{}, error) { 221 | kind := reflect.ValueOf(rawVal) 222 | if kind.Kind() == reflect.Map { 223 | result := make(map[string]interface{}) 224 | err := deserializeMapped(kind, result) 225 | if err != nil { 226 | return nil, err 227 | } 228 | 229 | return result, nil 230 | } 231 | return nil, errors.New("interface was not a map") 232 | } 233 | 234 | func deserializeNode(value serialization.ParseNode) (interface{}, error) { 235 | rawVal, err := value.GetRawValue() 236 | if err != nil { 237 | return nil, err 238 | } else { 239 | kind := reflect.ValueOf(rawVal) 240 | if kind.Kind() == reflect.Map { 241 | 242 | result := make(map[string]interface{}) 243 | err := deserializeMapped(kind, result) 244 | if err != nil { 245 | return nil, err 246 | } 247 | return result, nil 248 | } else { 249 | return deserializeValue(rawVal) 250 | } 251 | } 252 | } 253 | 254 | func deserializeMapped(v reflect.Value, result map[string]interface{}) error { 255 | for _, key := range v.MapKeys() { 256 | value, err := deserializeValue(v.MapIndex(key).Interface()) 257 | if err != nil { 258 | return err 259 | } else { 260 | result[key.String()] = value 261 | } 262 | } 263 | return nil 264 | } 265 | 266 | func deserializeNodes(value []*jsonserialization.JsonParseNode) (interface{}, error) { 267 | slice := make([]interface{}, len(value)) 268 | for index, element := range value { 269 | res, err := deserializeNode(element) 270 | if err != nil { 271 | return nil, err 272 | } 273 | slice[index] = res 274 | } 275 | return slice, nil 276 | } 277 | 278 | func deserializeValue(value interface{}) (interface{}, error) { 279 | switch v := value.(type) { 280 | case int: 281 | case float64: 282 | case string: 283 | return value, nil 284 | case *int: 285 | case *float64: 286 | case *string: 287 | return value, nil 288 | case jsonserialization.JsonParseNode: 289 | case *jsonserialization.JsonParseNode: 290 | return deserializeNode(v) 291 | case []*jsonserialization.JsonParseNode: 292 | return deserializeNodes(v) 293 | case []jsonserialization.JsonParseNode: 294 | return deserializeNodes(abs.CollectionApply(v, func(x jsonserialization.JsonParseNode) *jsonserialization.JsonParseNode { 295 | return &x 296 | })) 297 | default: 298 | return value, nil 299 | } 300 | return nil, nil 301 | } 302 | 303 | func castMapOfStrings(rawVal interface{}) (map[string]string, error) { 304 | result := make(map[string]string) 305 | v := reflect.ValueOf(rawVal) 306 | if v.Kind() == reflect.Map { 307 | for _, key := range v.MapKeys() { 308 | val, err := deserializeValue(v.MapIndex(key).Interface()) 309 | if err != nil { 310 | return nil, err 311 | } 312 | result[key.String()] = *(val.(*string)) 313 | } 314 | } 315 | return result, nil 316 | } 317 | 318 | // CreateBatchRequestItemDiscriminator creates a new instance of the appropriate class based on discriminator value 319 | func CreateBatchRequestItemDiscriminator(serialization.ParseNode) (serialization.Parsable, error) { 320 | var res batchItem 321 | return &res, nil 322 | } 323 | -------------------------------------------------------------------------------- /batch_request_collection.go: -------------------------------------------------------------------------------- 1 | package msgraphgocore 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | abstractions "github.com/microsoft/kiota-abstractions-go" 7 | ) 8 | 9 | type BatchRequestCollection struct { 10 | batchRequest *batchRequest 11 | batchLimit int 12 | } 13 | 14 | const MaxBatchRequests = 4 15 | 16 | // NewBatchRequestCollection creates an instance of a BatchRequestCollection with a default request limit 17 | func NewBatchRequestCollection(adapter abstractions.RequestAdapter) *BatchRequestCollection { 18 | return NewBatchRequestCollectionWithLimit(adapter, MaxBatchRequests) 19 | } 20 | 21 | // NewBatchRequestCollectionWithLimit creates an instance of a BatchRequestCollection with a defined limit in requests 22 | func NewBatchRequestCollectionWithLimit(adapter abstractions.RequestAdapter, batchLimit int) *BatchRequestCollection { 23 | return &BatchRequestCollection{ 24 | batchRequest: &batchRequest{ 25 | adapter: adapter, 26 | }, 27 | batchLimit: batchLimit, 28 | } 29 | } 30 | 31 | // AddBatchRequestStep converts RequestInformation to a BatchItem and adds it to a BatchRequestCollection 32 | func (b *BatchRequestCollection) AddBatchRequestStep(reqInfo abstractions.RequestInformation) (BatchItem, error) { 33 | return b.batchRequest.addLimitedBatchRequestStep(reqInfo, -1) 34 | } 35 | 36 | // Send serializes and sends the batch request to the server 37 | func (b *BatchRequestCollection) Send(ctx context.Context, adapter abstractions.RequestAdapter) (BatchResponse, error) { 38 | // spit request with a max of 19 39 | requestItems := chunkSlice(b.batchRequest.requests, 19) 40 | 41 | if len(requestItems) > b.batchLimit { 42 | return nil, errors.New("exceeded max number of batch requests") 43 | } 44 | 45 | // execute requests 46 | response := NewBatchResponse() 47 | for _, requests := range requestItems { 48 | batch := NewBatchRequest(b.batchRequest.adapter) 49 | batch.SetRequests(requests) 50 | res, err := batch.Send(ctx, adapter) 51 | if err != nil { 52 | return nil, err 53 | } 54 | response.AddResponses(res.GetResponses()) 55 | } 56 | 57 | return response, nil 58 | } 59 | 60 | func chunkSlice[T interface{}](slice []T, chunkSize int) [][]T { 61 | var chunks [][]T 62 | for i := 0; i < len(slice); i += chunkSize { 63 | end := i + chunkSize 64 | if end > len(slice) { 65 | end = len(slice) 66 | } 67 | 68 | chunks = append(chunks, slice[i:end]) 69 | } 70 | return chunks 71 | } 72 | -------------------------------------------------------------------------------- /batch_request_collection_test.go: -------------------------------------------------------------------------------- 1 | package msgraphgocore 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | ) 12 | 13 | func TestNewBatchRequestCollectionNoLimit(t *testing.T) { 14 | batch := NewBatchRequestCollection(reqAdapter) 15 | reqInfo := getRequestInfo() 16 | 17 | for i := 0; i < 20; i++ { 18 | _, err := batch.AddBatchRequestStep(*reqInfo) 19 | if err != nil { 20 | return 21 | } 22 | } 23 | 24 | _, err := batch.AddBatchRequestStep(*reqInfo) 25 | assert.Nil(t, err) 26 | } 27 | 28 | func TestBatchRequestCollectionReturnsBatchResponse(t *testing.T) { 29 | testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 30 | w.Header().Set("Content-Type", "application/json") 31 | jsonResponse := getDummyJSON() 32 | w.WriteHeader(200) 33 | fmt.Fprint(w, jsonResponse) 34 | })) 35 | defer testServer.Close() 36 | 37 | reqInfo := getRequestInfo() 38 | 39 | mockPath := testServer.URL + "/$batch" 40 | reqAdapter.SetBaseUrl(mockPath) // check that path is not empty instead 41 | 42 | batch := NewBatchRequestCollection(reqAdapter) 43 | for i := 0; i < 40; i++ { 44 | _, err := batch.AddBatchRequestStep(*reqInfo) 45 | if err != nil { 46 | require.NoError(t, err) 47 | } 48 | } 49 | 50 | resp, err := batch.Send(context.Background(), reqAdapter) 51 | require.NoError(t, err) 52 | 53 | assert.Equal(t, len(resp.GetResponses()), 12) 54 | } 55 | 56 | func TestBatchRequestResponseGetFailedResponses(t *testing.T) { 57 | testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 58 | w.Header().Set("Content-Type", "application/json") 59 | jsonResponse := getDummyJSON() 60 | w.WriteHeader(200) 61 | fmt.Fprint(w, jsonResponse) 62 | })) 63 | defer testServer.Close() 64 | 65 | reqInfo := getRequestInfo() 66 | 67 | mockPath := testServer.URL + "/$batch" 68 | reqAdapter.SetBaseUrl(mockPath) // check that path is not empty instead 69 | 70 | batch := NewBatchRequestCollection(reqAdapter) 71 | _, err := batch.AddBatchRequestStep(*reqInfo) 72 | require.NoError(t, err) 73 | 74 | resp, err := batch.Send(context.Background(), reqAdapter) 75 | require.NoError(t, err) 76 | 77 | assert.Equal(t, len(resp.GetStatusCodes()), 4) 78 | 79 | status := resp.GetFailedResponses() 80 | assert.Equal(t, 1, len(status)) 81 | } 82 | -------------------------------------------------------------------------------- /batch_request_test.go: -------------------------------------------------------------------------------- 1 | package msgraphgocore 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "net/http/httptest" 9 | "net/url" 10 | "testing" 11 | 12 | "github.com/microsoft/kiota-abstractions-go/serialization" 13 | "github.com/microsoftgraph/msgraph-sdk-go-core/internal" 14 | 15 | abstractions "github.com/microsoft/kiota-abstractions-go" 16 | "github.com/stretchr/testify/assert" 17 | "github.com/stretchr/testify/require" 18 | ) 19 | 20 | func p[T interface{}](t T) *T { 21 | return &t 22 | } 23 | 24 | func TestConstructionOfRequests(t *testing.T) { 25 | reqInfo := getRequestInfo() 26 | 27 | batch := NewBatchRequest(reqAdapter) 28 | 29 | item1, err := batch.AddBatchRequestStep(*reqInfo) 30 | require.NoError(t, err) 31 | 32 | item2, err := batch.AddBatchRequestStep(*reqInfo) 33 | require.NoError(t, err) 34 | 35 | assert.Equal(t, len(batch.GetRequests()), 2) 36 | assert.Equal(t, batch.GetRequests()[0], item1) 37 | assert.Equal(t, batch.GetRequests()[1], item2) 38 | } 39 | 40 | func TestRegisteringDependsOn(t *testing.T) { 41 | 42 | reqInfo1 := getRequestInfo() 43 | reqInfo2 := getRequestInfo() 44 | 45 | batch := NewBatchRequest(reqAdapter) 46 | batchItem1, err := batch.AddBatchRequestStep(*reqInfo1) 47 | require.NoError(t, err) 48 | 49 | batchItem2, err := batch.AddBatchRequestStep(*reqInfo2) 50 | require.NoError(t, err) 51 | 52 | batchItem2.DependsOnItem(batchItem1) 53 | 54 | assert.Equal(t, batchItem2.GetDependsOn(), []string{*batchItem1.GetId()}) 55 | } 56 | 57 | func TestReturnsBatchResponse(t *testing.T) { 58 | testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 59 | w.Header().Set("Content-Type", "application/json") 60 | jsonResponse := getDummyJSON() 61 | w.WriteHeader(200) 62 | fmt.Fprint(w, jsonResponse) 63 | })) 64 | defer testServer.Close() 65 | 66 | reqInfo := getRequestInfo() 67 | 68 | mockPath := testServer.URL + "/$batch" 69 | reqAdapter.SetBaseUrl(mockPath) // check that path is not empty instead 70 | 71 | batch := NewBatchRequest(reqAdapter) 72 | _, err := batch.AddBatchRequestStep(*reqInfo) 73 | require.NoError(t, err) 74 | 75 | resp, err := batch.Send(context.Background(), reqAdapter) 76 | require.NoError(t, err) 77 | 78 | assert.Equal(t, len(resp.GetResponses()), 4) 79 | } 80 | 81 | func TestContentSentToServer(t *testing.T) { 82 | testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 83 | w.Header().Set("Content-Type", "application/json") 84 | jsonResponse := getDummyJSON() 85 | w.WriteHeader(200) 86 | fmt.Fprint(w, jsonResponse) 87 | })) 88 | defer testServer.Close() 89 | 90 | reqInfo := getRequestInfo() 91 | 92 | mockPath := testServer.URL + "/$batch" 93 | reqAdapter.SetBaseUrl(mockPath) // check that path is not empty instead 94 | 95 | batch := NewBatchRequest(reqAdapter) 96 | item, err := batch.AddBatchRequestStep(*reqInfo) 97 | item.SetId(p("123")) 98 | require.NoError(t, err) 99 | 100 | baseUrl, err := getBaseUrl(reqAdapter) 101 | require.NoError(t, err) 102 | 103 | requestInfo, err := buildRequestInfo(context.Background(), reqAdapter, batch, baseUrl) 104 | require.NoError(t, err) 105 | content := string(requestInfo.Content) 106 | expected := "{\"requests\":[{\"id\":\"123\",\"method\":\"GET\",\"url\":\"\",\"headers\":{\"content-type\":\"application/json\"},\"body\":{\"username\":\"name\"},\"dependsOn\":[]}]}" 107 | assert.Equal(t, expected, content) 108 | 109 | resp, err := batch.Send(context.Background(), reqAdapter) 110 | require.NoError(t, err) 111 | 112 | assert.Equal(t, len(resp.GetResponses()), 4) 113 | } 114 | 115 | func TestRespectsBatchItemLimitOf20BatchItems(t *testing.T) { 116 | batch := NewBatchRequest(reqAdapter) 117 | reqInfo := getRequestInfo() 118 | 119 | for i := 0; i < 20; i++ { 120 | _, err := batch.AddBatchRequestStep(*reqInfo) 121 | if err != nil { 122 | return 123 | } 124 | } 125 | 126 | _, err := batch.AddBatchRequestStep(*reqInfo) 127 | assert.Equal(t, err.Error(), "batch items limit exceeded. BatchRequest has a limit of 20 batch items") 128 | } 129 | 130 | func TestHandlesUnhandledHTTPError(t *testing.T) { 131 | testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 132 | w.Header().Set("Content-Type", "application/json") 133 | w.WriteHeader(403) 134 | fmt.Fprint(w, "") 135 | })) 136 | defer testServer.Close() 137 | 138 | mockPath := testServer.URL + "/$batch" 139 | reqAdapter.SetBaseUrl(mockPath) 140 | 141 | reqInfo := getRequestInfo() 142 | batch := NewBatchRequest(reqAdapter) 143 | _, err := batch.AddBatchRequestStep(*reqInfo) 144 | require.NoError(t, err) 145 | 146 | _, err = batch.Send(context.Background(), reqAdapter) 147 | assert.Equal(t, err.Error(), "The server returned an unexpected status code and no error factory is registered for this code: 403") 148 | } 149 | 150 | func TestHandlesHTTPError(t *testing.T) { 151 | testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 152 | w.Header().Set("Content-Type", "application/json") 153 | w.WriteHeader(403) 154 | fmt.Fprint(w, "{}") 155 | })) 156 | defer testServer.Close() 157 | 158 | mockPath := testServer.URL + "/$batch" 159 | reqAdapter.SetBaseUrl(mockPath) 160 | 161 | reqInfo := getRequestInfo() 162 | batch := NewBatchRequest(reqAdapter) 163 | _, err := batch.AddBatchRequestStep(*reqInfo) 164 | require.NoError(t, err) 165 | 166 | errorMapping := abstractions.ErrorMappings{ 167 | "4XX": internal.CreateSampleErrorFromDiscriminatorValue, 168 | "5XX": internal.CreateSampleErrorFromDiscriminatorValue, 169 | } 170 | // register errorMapper 171 | err = RegisterError(BatchRequestErrorRegistryKey, errorMapping) 172 | require.NoError(t, err) 173 | 174 | _, err = batch.Send(context.Background(), reqAdapter) 175 | 176 | var sampleError *internal.SampleError 177 | switch { 178 | case errors.As(err, &sampleError): 179 | assert.Equal(t, "error status code received from the API", err.Error()) 180 | default: 181 | assert.Fail(t, "error type is not as expected") 182 | } 183 | 184 | err = DeRegisterError(BatchRequestErrorRegistryKey) 185 | require.NoError(t, err) 186 | } 187 | 188 | func TestGetResponseByIdForSuccessfulRequest(t *testing.T) { 189 | mockResponse := `{ 190 | "responses": [ 191 | { 192 | "id": "2", 193 | "status": 200, 194 | "body": { 195 | "username": "testuser", 196 | "person" : { 197 | "firstName" : "Tony", 198 | "lastName" : "Blair", 199 | "active" : false, 200 | "bankBalance": 234234.67, 201 | "accounts" : [1,2,3], 202 | "positions" : ["Prime","Minister"], 203 | "children" : [ 204 | { 205 | "firstName" : "Kathryn", 206 | "lastName" : "Blair" 207 | }, 208 | { 209 | "firstName" : "Euan", 210 | "lastName" : "Blair" 211 | } 212 | ] 213 | } 214 | } 215 | } 216 | ] 217 | }` 218 | mockServer := makeMockRequest(200, mockResponse) 219 | defer mockServer.Close() 220 | 221 | mockPath := mockServer.URL + "/$batch" 222 | reqAdapter.SetBaseUrl(mockPath) 223 | 224 | reqInfo := getRequestInfo() 225 | batch := NewBatchRequest(reqAdapter) 226 | _, err := batch.AddBatchRequestStep(*reqInfo) 227 | if err != nil { 228 | return 229 | } 230 | 231 | resp, err := batch.Send(context.Background(), reqAdapter) 232 | require.NoError(t, err) 233 | 234 | user, err := GetBatchResponseById[Userable](resp, "2", CreateUser) 235 | require.NoError(t, err) 236 | 237 | assert.Equal(t, *(user.GetUserName()), "testuser") 238 | } 239 | 240 | type Userable interface { 241 | serialization.Parsable 242 | GetUserName() *string 243 | SetUserName(*string) 244 | } 245 | 246 | type User struct { 247 | UserName *string 248 | Person *Person 249 | } 250 | 251 | func (u *User) GetUserName() *string { 252 | return u.UserName 253 | } 254 | 255 | func (u *User) SetUserName(userName *string) { 256 | u.UserName = userName 257 | } 258 | 259 | func (u *User) Serialize(writer serialization.SerializationWriter) error { 260 | panic("implement me") 261 | } 262 | 263 | func (u *User) GetFieldDeserializers() map[string]func(serialization.ParseNode) error { 264 | res := make(map[string]func(serialization.ParseNode) error) 265 | res["username"] = func(n serialization.ParseNode) error { 266 | val, err := n.GetStringValue() 267 | if err != nil { 268 | return err 269 | } 270 | if val != nil { 271 | u.SetUserName(val) 272 | } 273 | return nil 274 | } 275 | return res 276 | } 277 | 278 | func CreateUser(parseNode serialization.ParseNode) (serialization.Parsable, error) { 279 | return &User{}, nil 280 | } 281 | 282 | type Person struct { 283 | FirstName string `json:"firstName"` 284 | LastName string `json:"lastName"` 285 | Active bool `json:"active"` 286 | Positions []*string `json:"positions"` 287 | BankBalance *float64 `json:"bankBalance"` 288 | Accounts []*int `json:"accounts"` 289 | Children []*Person `json:"children"` 290 | } 291 | 292 | func (u Person) Serialize(writer serialization.SerializationWriter) error { 293 | return nil 294 | } 295 | 296 | func (u Person) GetFieldDeserializers() map[string]func(serialization.ParseNode) error { 297 | return make(map[string]func(serialization.ParseNode) error) 298 | } 299 | 300 | func TestGetResponseByIdFailedRequest(t *testing.T) { 301 | mockServer := makeMockRequest(200, getDummyJSON()) 302 | defer mockServer.Close() 303 | 304 | mockPath := mockServer.URL + "/$batch" 305 | reqAdapter.SetBaseUrl(mockPath) 306 | 307 | reqInfo := getRequestInfo() 308 | batch := NewBatchRequest(reqAdapter) 309 | _, err := batch.AddBatchRequestStep(*reqInfo) 310 | require.NoError(t, err) 311 | 312 | resp, err := batch.Send(context.Background(), reqAdapter) 313 | require.NoError(t, err) 314 | 315 | _, err = GetBatchResponseById[Userable](resp, "3", CreateUser) 316 | assert.Equal(t, "The server returned an unexpected status code and no error factory is registered for this code: 401", err.Error()) 317 | } 318 | 319 | func TestGetErrorResponseBodyById(t *testing.T) { 320 | var jsonBlob = `{ 321 | "responses": [{ 322 | "id": "3", 323 | "status": 400, 324 | "headers": { 325 | "Content-Type": "application/json" 326 | }, 327 | "body": { 328 | "error": { 329 | "code": "ExtensionError", 330 | "message": "Exception: [Status Code: BadRequest; Reason: Boom]", 331 | "innerError": { 332 | "request-id": "123" 333 | } 334 | } 335 | } 336 | }] 337 | }` 338 | 339 | errorMapping := abstractions.ErrorMappings{ 340 | "4XX": internal.CreateSampleErrorFromDiscriminatorValue, 341 | "5XX": internal.CreateSampleErrorFromDiscriminatorValue, 342 | } 343 | err := RegisterError("Userable", errorMapping) 344 | assert.NoError(t, err) 345 | 346 | mockServer := makeMockRequest(200, jsonBlob) 347 | defer mockServer.Close() 348 | 349 | mockPath := mockServer.URL + "/$batch" 350 | reqAdapter.SetBaseUrl(mockPath) 351 | 352 | reqInfo := getRequestInfo() 353 | batch := NewBatchRequest(reqAdapter) 354 | _, err = batch.AddBatchRequestStep(*reqInfo) 355 | require.NoError(t, err) 356 | 357 | resp, err := batch.Send(context.Background(), reqAdapter) 358 | require.NoError(t, err) 359 | 360 | _, err = GetBatchResponseById[Userable](resp, "3", CreateUser) 361 | serr := &internal.SampleError{} 362 | assert.ErrorAs(t, err, &serr) 363 | assert.Equal(t, "Exception: [Status Code: BadRequest; Reason: Boom]", serr.Message) 364 | 365 | err = DeRegisterError("Userable") 366 | require.NoError(t, err) 367 | } 368 | 369 | func TestGetResponseByIdFailedRequestWithFactory(t *testing.T) { 370 | mockServer := makeMockRequest(200, getDummyJSON()) 371 | defer mockServer.Close() 372 | 373 | mockPath := mockServer.URL + "/$batch" 374 | reqAdapter.SetBaseUrl(mockPath) 375 | 376 | errorMapping := abstractions.ErrorMappings{ 377 | "4XX": internal.CreateSampleErrorFromDiscriminatorValue, 378 | "5XX": internal.CreateSampleErrorFromDiscriminatorValue, 379 | } 380 | // register errorMapper 381 | err := RegisterError(BatchRequestErrorRegistryKey, errorMapping) 382 | require.NoError(t, err) 383 | 384 | reqInfo := getRequestInfo() 385 | batch := NewBatchRequest(reqAdapter) 386 | _, err = batch.AddBatchRequestStep(*reqInfo) 387 | require.NoError(t, err) 388 | 389 | resp, err := batch.Send(context.Background(), reqAdapter) 390 | require.NoError(t, err) 391 | 392 | _, err = GetBatchResponseById[Userable](resp, "3", CreateUser) 393 | assert.Equal(t, "The server returned an unexpected status code with no response body: 401", err.Error()) 394 | 395 | err = DeRegisterError(BatchRequestErrorRegistryKey) 396 | require.NoError(t, err) 397 | } 398 | 399 | func makeMockRequest(mockStatus int, mockResponse string) *httptest.Server { 400 | return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 401 | w.Header().Set("Content-Type", "application/json") 402 | w.WriteHeader(mockStatus) 403 | fmt.Fprint(w, mockResponse) 404 | })) 405 | } 406 | 407 | func getRequestInfo() *abstractions.RequestInformation { 408 | content := ` 409 | { 410 | "username": "name" 411 | } 412 | ` 413 | reqInfo := abstractions.NewRequestInformation() 414 | reqInfo.SetUri(url.URL{}) 415 | reqInfo.Content = []byte(content) 416 | reqInfo.UrlTemplate = "{+baseurl}/$batch" 417 | headers := abstractions.NewRequestHeaders() 418 | headers.Add("Content-Type", "application/json") 419 | reqInfo.Headers.AddAll(headers) 420 | 421 | return reqInfo 422 | } 423 | 424 | func getDummyJSON() string { 425 | return `{ 426 | "responses": [ 427 | { 428 | "id": "1", 429 | "status": 302, 430 | "body": null, 431 | "headers": { 432 | "location": "https://b0mpua-by3301.files.1drv.com/y23vmagahszhxzlcvhasdhasghasodfi" 433 | } 434 | }, 435 | { 436 | "id": "3", 437 | "status": 401, 438 | "body": { 439 | "error": { 440 | "code": "Forbidden", 441 | "message": "Insufficient permissions" 442 | } 443 | } 444 | }, 445 | { 446 | "id": "2", 447 | "status": 200, 448 | "body": { 449 | "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#Collection(microsoft.graph.plannerTask)", 450 | "value": [] 451 | } 452 | }, 453 | { 454 | "id": "4", 455 | "status": 204, 456 | "url": "https://graph.microsoft.com/v1.0/$metadata#Collection(microsoft.graph.plannerTask)", 457 | "body": null 458 | } 459 | ] 460 | }` 461 | } 462 | -------------------------------------------------------------------------------- /batch_requests.go: -------------------------------------------------------------------------------- 1 | package msgraphgocore 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/gob" 7 | "encoding/json" 8 | "errors" 9 | "net/url" 10 | "reflect" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/google/uuid" 15 | abs "github.com/microsoft/kiota-abstractions-go" 16 | abstractions "github.com/microsoft/kiota-abstractions-go" 17 | "github.com/microsoft/kiota-abstractions-go/serialization" 18 | absser "github.com/microsoft/kiota-abstractions-go/serialization" 19 | nethttplibrary "github.com/microsoft/kiota-http-go" 20 | ) 21 | 22 | const BatchRequestErrorRegistryKey = "BATCH_REQUEST_ERROR_REGISTRY_KEY" 23 | const jsonContentType = "application/json" 24 | 25 | // RequestHeader is a type alias for http request headers 26 | type RequestHeader map[string]string 27 | 28 | // Serialize serializes information the current object 29 | func (br RequestHeader) Serialize(writer serialization.SerializationWriter) error { 30 | return nil 31 | } 32 | 33 | // GetFieldDeserializers the deserialization information for the current model 34 | func (br RequestHeader) GetFieldDeserializers() map[string]func(serialization.ParseNode) error { 35 | return make(map[string]func(serialization.ParseNode) error) 36 | } 37 | 38 | // RequestBody is a type alias for http request bodies 39 | type RequestBody map[string]interface{} 40 | 41 | // Serialize serializes information the current object 42 | func (br RequestBody) Serialize(writer serialization.SerializationWriter) error { 43 | return nil 44 | } 45 | 46 | // GetFieldDeserializers the deserialization information for the current model 47 | func (br RequestBody) GetFieldDeserializers() map[string]func(serialization.ParseNode) error { 48 | return make(map[string]func(serialization.ParseNode) error) 49 | } 50 | 51 | type batchRequest struct { 52 | requests []BatchItem 53 | adapter abstractions.RequestAdapter 54 | } 55 | 56 | // NewBatchRequest creates an instance of BatchRequest 57 | func NewBatchRequest(adapter abstractions.RequestAdapter) BatchRequest { 58 | return &batchRequest{ 59 | adapter: adapter, 60 | } 61 | } 62 | 63 | // BatchRequest models all the properties of a batch request 64 | type BatchRequest interface { 65 | serialization.Parsable 66 | GetRequests() []BatchItem 67 | SetRequests(requests []BatchItem) 68 | AddBatchRequestStep(reqInfo abstractions.RequestInformation) (BatchItem, error) 69 | Send(ctx context.Context, adapter abstractions.RequestAdapter) (BatchResponse, error) 70 | } 71 | 72 | // GetRequests return all the Items in the batch request 73 | func (br *batchRequest) GetRequests() []BatchItem { 74 | return br.requests 75 | } 76 | 77 | // SetRequests add a collection of requests to the batch Items 78 | func (br *batchRequest) SetRequests(requests []BatchItem) { 79 | br.requests = requests 80 | } 81 | 82 | // Serialize serializes information the current object 83 | func (br *batchRequest) Serialize(writer serialization.SerializationWriter) error { 84 | { 85 | cast := abs.CollectionApply(br.requests, func(v BatchItem) serialization.Parsable { 86 | return v.(serialization.Parsable) 87 | }) 88 | err := writer.WriteCollectionOfObjectValues("requests", cast) 89 | if err != nil { 90 | return err 91 | } 92 | } 93 | return nil 94 | } 95 | 96 | // GetFieldDeserializers the deserialization information for the current model 97 | func (br *batchRequest) GetFieldDeserializers() map[string]func(serialization.ParseNode) error { 98 | return make(map[string]func(serialization.ParseNode) error) 99 | } 100 | 101 | // AddBatchRequestStep converts RequestInformation to a BatchItem and adds it to a BatchRequest 102 | // 103 | // You can add upto 20 BatchItems to a BatchRequest 104 | func (br *batchRequest) AddBatchRequestStep(reqInfo abstractions.RequestInformation) (BatchItem, error) { 105 | return br.addLimitedBatchRequestStep(reqInfo, 19) 106 | } 107 | 108 | func (br *batchRequest) addLimitedBatchRequestStep(reqInfo abstractions.RequestInformation, requestLimit int) (BatchItem, error) { 109 | if requestLimit != -1 && len(br.GetRequests()) > requestLimit { 110 | return nil, errors.New("batch items limit exceeded. BatchRequest has a limit of 20 batch items") 111 | } 112 | 113 | batchItem, err := br.toBatchItem(reqInfo) 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | br.SetRequests(append(br.GetRequests(), batchItem)) 119 | return batchItem, nil 120 | } 121 | 122 | func (br *batchRequest) toBatchItem(requestInfo abstractions.RequestInformation) (BatchItem, error) { 123 | if _, ok := requestInfo.PathParameters["baseurl"]; !ok { 124 | // address issue for request information missing baseUrl 125 | // https://github.com/microsoft/kiota/issues/2061 126 | requestInfo.PathParameters["baseurl"] = br.adapter.GetBaseUrl() 127 | } 128 | 129 | uri, err := requestInfo.GetUri() 130 | if err != nil { 131 | return nil, err 132 | } 133 | uriString := nethttplibrary.ReplacePathTokens(uri.String(), ReplacementPairs) 134 | 135 | var body map[string]interface{} 136 | if requestInfo.Content != nil { 137 | err = json.Unmarshal(requestInfo.Content, &body) 138 | if err != nil { 139 | return nil, err 140 | } 141 | } 142 | 143 | newID := uuid.NewString() 144 | method := requestInfo.Method.String() 145 | 146 | request := NewBatchItem() 147 | request.SetId(&newID) 148 | request.SetMethod(&method) 149 | request.SetBody(body) 150 | headers := make(map[string]string) 151 | for _, key := range requestInfo.Headers.ListKeys() { 152 | value := requestInfo.Headers.Get(key) 153 | headers[key] = strings.Join(value, ",") 154 | } 155 | request.SetHeaders(headers) 156 | 157 | baseUri, err := getBaseUrl(br.adapter) 158 | if err != nil { 159 | return nil, err 160 | } 161 | var finalUrl = strings.Replace(uriString, baseUri.String(), "", 1) 162 | request.SetUrl(&finalUrl) 163 | 164 | return request, nil 165 | } 166 | 167 | // Send serializes and sends the batch request to the server 168 | func (br *batchRequest) Send(ctx context.Context, adapter abstractions.RequestAdapter) (BatchResponse, error) { 169 | baseUrl, err := getBaseUrl(adapter) 170 | if err != nil { 171 | return nil, err 172 | } 173 | 174 | requestInfo, err := buildRequestInfo(ctx, adapter, br, baseUrl) 175 | if err != nil { 176 | return nil, err 177 | } 178 | return sendBatchRequest(ctx, requestInfo, adapter) 179 | } 180 | 181 | func getBaseUrl(adapter abstractions.RequestAdapter) (*url.URL, error) { 182 | return url.Parse(adapter.GetBaseUrl()) 183 | } 184 | 185 | func buildRequestInfo(ctx context.Context, adapter abstractions.RequestAdapter, body BatchRequest, baseUrl *url.URL) (*abstractions.RequestInformation, error) { 186 | requestInfo := abstractions.NewRequestInformation() 187 | requestInfo.Method = abstractions.POST 188 | requestInfo.UrlTemplate = "{+baseurl}/$batch" 189 | err := requestInfo.SetContentFromParsable(ctx, adapter, "application/json", body) 190 | if err != nil { 191 | return nil, err 192 | } 193 | requestInfo.Headers.Add("Content-Type", "application/json") 194 | 195 | return requestInfo, nil 196 | } 197 | 198 | func getResponsePrimaryContentType(responseItem BatchItem) string { 199 | header := responseItem.GetHeaders() 200 | if header == nil { 201 | return "" 202 | } 203 | rawType := header["Content-Type"] 204 | splat := strings.Split(rawType, ";") 205 | return strings.ToLower(splat[0]) 206 | } 207 | 208 | func getRootParseNode(responseItem BatchItem) (absser.ParseNode, error) { 209 | contentType := getResponsePrimaryContentType(responseItem) 210 | if contentType == "" { 211 | return nil, nil 212 | } 213 | 214 | var ( 215 | content []byte 216 | err error 217 | ) 218 | if contentType == jsonContentType { 219 | if content, err = json.Marshal(responseItem.GetBody()); err != nil { 220 | return nil, err 221 | } 222 | } else { 223 | var buf bytes.Buffer 224 | if err = gob.NewEncoder(&buf).Encode(responseItem.GetBody()); err != nil { 225 | return nil, err 226 | } 227 | content = buf.Bytes() 228 | } 229 | 230 | return serialization.DefaultParseNodeFactoryInstance.GetRootParseNode(contentType, content) 231 | } 232 | 233 | func throwErrors(responseItem BatchItem, typeName string) error { 234 | errorMappings := getErrorMapper(typeName) 235 | if errorMappings == nil { 236 | errorMappings = getErrorMapper(BatchRequestErrorRegistryKey) 237 | } 238 | responseStatus := *responseItem.GetStatus() 239 | 240 | statusAsString := strconv.Itoa(int(responseStatus)) 241 | var errorCtor absser.ParsableFactory = nil 242 | if len(errorMappings) != 0 { 243 | if responseStatus >= 400 && responseStatus < 500 && errorMappings["4XX"] != nil { 244 | errorCtor = errorMappings["4XX"] 245 | } else if responseStatus >= 500 && responseStatus < 600 && errorMappings["5XX"] != nil { 246 | errorCtor = errorMappings["5XX"] 247 | } 248 | } 249 | 250 | if errorCtor == nil { 251 | return &abstractions.ApiError{ 252 | Message: "The server returned an unexpected status code and no error factory is registered for this code: " + statusAsString, 253 | } 254 | } 255 | 256 | rootNode, err := getRootParseNode(responseItem) 257 | if err != nil { 258 | return err 259 | } 260 | if rootNode == nil { 261 | return &abstractions.ApiError{ 262 | Message: "The server returned an unexpected status code with no response body: " + statusAsString, 263 | } 264 | } 265 | 266 | errValue, err := rootNode.GetObjectValue(errorCtor) 267 | if err != nil { 268 | return err 269 | } 270 | 271 | return errValue.(error) 272 | } 273 | 274 | // GetBatchResponseById returns the response of the batch request item with the given id. 275 | func GetBatchResponseById[T serialization.Parsable](resp BatchResponse, itemId string, constructor absser.ParsableFactory) (T, error) { 276 | var res T 277 | item := resp.GetResponseById(itemId) 278 | 279 | if *item.GetStatus() >= 400 { 280 | return res, throwErrors(item, reflect.TypeOf(new(T)).Elem().Name()) 281 | } 282 | 283 | jsonStr, err := json.Marshal(item.GetBody()) 284 | if err != nil { 285 | return res, err 286 | } 287 | 288 | var parseNodeFactory = absser.DefaultParseNodeFactoryInstance 289 | 290 | parseNode, err := parseNodeFactory.GetRootParseNode(jsonContentType, jsonStr) 291 | if err != nil { 292 | return res, err 293 | } 294 | 295 | result, err := parseNode.GetObjectValue(constructor) 296 | return result.(T), err 297 | } 298 | 299 | func getErrorMapper(key string) abstractions.ErrorMappings { 300 | errorMapperSrc, found := GetErrorFactoryFromRegistry(key) 301 | if found { 302 | return errorMapperSrc 303 | } 304 | return nil 305 | } 306 | 307 | func sendBatchRequest(ctx context.Context, requestInfo *abstractions.RequestInformation, adapter abstractions.RequestAdapter) (BatchResponse, error) { 308 | if requestInfo == nil { 309 | return nil, errors.New("requestInfo cannot be nil") 310 | } 311 | 312 | response, err := adapter.Send(ctx, requestInfo, CreateBatchResponseDiscriminator, getErrorMapper(BatchRequestErrorRegistryKey)) 313 | if err != nil { 314 | return nil, err 315 | } 316 | 317 | return response.(BatchResponse), nil 318 | } 319 | -------------------------------------------------------------------------------- /batch_response_model.go: -------------------------------------------------------------------------------- 1 | package msgraphgocore 2 | 3 | import ( 4 | "github.com/microsoft/kiota-abstractions-go/serialization" 5 | ) 6 | 7 | type batchResponse struct { 8 | responses []BatchItem 9 | indexResponse map[string]BatchItem 10 | isIndexed bool 11 | } 12 | 13 | func NewBatchResponse() BatchResponse { 14 | return &batchResponse{ 15 | indexResponse: make(map[string]BatchItem), 16 | isIndexed: false, 17 | } 18 | } 19 | 20 | // GetResponses returns a slice of BatchItem to the user 21 | func (br *batchResponse) GetResponses() []BatchItem { 22 | return br.responses 23 | } 24 | 25 | // SetResponses adds a slice of BatchItem to the response 26 | func (br *batchResponse) SetResponses(responses []BatchItem) { 27 | br.responses = responses 28 | } 29 | 30 | // AddResponses adds elements to existing response 31 | func (br *batchResponse) AddResponses(responses []BatchItem) { 32 | for _, v := range responses { 33 | br.responses = append(br.responses, v) 34 | } 35 | } 36 | 37 | // GetResponseById returns a response payload as a batch item 38 | func (br *batchResponse) GetResponseById(itemId string) BatchItem { 39 | if !br.isIndexed { 40 | 41 | for _, resp := range br.GetResponses() { 42 | br.indexResponse[*(resp.GetId())] = resp 43 | } 44 | 45 | br.isIndexed = true 46 | } 47 | 48 | return br.indexResponse[itemId] 49 | } 50 | 51 | // CreateBatchResponseDiscriminator creates a new instance of the appropriate class based on discriminator value 52 | func CreateBatchResponseDiscriminator(serialization.ParseNode) (serialization.Parsable, error) { 53 | return NewBatchResponse(), nil 54 | } 55 | 56 | // BatchResponse instance of batch request result payload 57 | type BatchResponse interface { 58 | serialization.Parsable 59 | GetResponses() []BatchItem 60 | SetResponses(responses []BatchItem) 61 | AddResponses(responses []BatchItem) 62 | GetResponseById(itemId string) BatchItem 63 | GetFailedResponses() map[string]int32 64 | GetStatusCodes() map[string]int32 65 | } 66 | 67 | // Serialize serializes information the current object 68 | func (br *batchResponse) Serialize(serialization.SerializationWriter) error { 69 | panic("batch responses are not serializable") 70 | } 71 | 72 | // GetFieldDeserializers the deserialization information for the current model 73 | func (br *batchResponse) GetFieldDeserializers() map[string]func(serialization.ParseNode) error { 74 | res := make(map[string]func(serialization.ParseNode) error) 75 | res["responses"] = func(n serialization.ParseNode) error { 76 | val, err := n.GetCollectionOfObjectValues(CreateBatchRequestItemDiscriminator) 77 | if err != nil { 78 | return err 79 | } 80 | if val != nil { 81 | res := make([]BatchItem, len(val)) 82 | for i, v := range val { 83 | res[i] = v.(BatchItem) 84 | } 85 | br.SetResponses(res) 86 | } 87 | return nil 88 | } 89 | return res 90 | } 91 | 92 | // GetFailedResponses returns a map of responses that failed 93 | func (br *batchResponse) GetFailedResponses() map[string]int32 { 94 | statuses := make(map[string]int32) 95 | for _, response := range br.GetResponses() { 96 | if *response.GetStatus() > 399 && *response.GetStatus() < 600 { 97 | statuses[*response.GetId()] = *response.GetStatus() 98 | } 99 | } 100 | return statuses 101 | } 102 | 103 | // GetStatusCodes returns a map of responses statuses and the status codes 104 | func (br *batchResponse) GetStatusCodes() map[string]int32 { 105 | statuses := make(map[string]int32) 106 | for _, response := range br.GetResponses() { 107 | statuses[*response.GetId()] = *response.GetStatus() 108 | } 109 | return statuses 110 | } 111 | -------------------------------------------------------------------------------- /docs/batch_request.md: -------------------------------------------------------------------------------- 1 | ## BatchRequest 2 | 3 | BatchRequest is useful when you want to make multiple requests efficiently. It batches all requests (upto 20 requests) into a json object and makes one api call. You can learn more about it on [Microsoft Docs](https://docs.microsoft.com/en-us/graph/json-batching). 4 | 5 | ## Code Sample 6 | 7 | ```go 8 | import "github.com/microsoftgraph/msgraph-sdk-go-core" 9 | import abstractions "github.com/microsoft/kiota-abstractions-go" 10 | 11 | reqInfo := client.Me().CreateGetRequestInformation() 12 | batch := msgraphsdkcore.NewBatchRequest() 13 | batchItem := batch.AppendBatchItem(*reqInfo) 14 | 15 | resp, err := batch.Send(reqAdapter) 16 | 17 | // print the first response 18 | user := GetBatchResponseById[User](resp, "1", CreateUserFromDiscriminatorValue) // returns a serialized response 19 | fmt.Println(user.GetDisplayName()) // Print display name 20 | ``` 21 | 22 | ## Depends On Relationship 23 | 24 | BatchItem supports constructing a dependency chain for scenarios where you want one request to be sent out before another request is made. In the example below batchItem2 will be sent before batchItem1. 25 | 26 | ```go 27 | batchItem1 := batch.AppendBatchItem(*reqInfo) 28 | batchItem2 := batch.AppendBatchItem(*reqInfo) 29 | 30 | batchItem1.DependsOnItem(batchItem2) 31 | ``` 32 | 33 | ## Adds BatchCollectionResponse 34 | 35 | `BatchRequestCollection` allows users to add more than 19 requests and send them as multiple `BatchRequest`'s. The send functionality of BatchRequestCollection splits the requests and sends them in serial. 36 | 37 | ```go 38 | batchCollection := msgraphgocore.NewBatchRequestCollection(client.GetAdapter()) 39 | 40 | meRequestItem, _ := batchCollection.AddBatchRequestStep(*meRequest) 41 | eventsRequestItem, _ := batchCollection.AddBatchRequestStep(*eventsRequest) 42 | 43 | batchResponse, _ := batchCollection.Send(context.Background(), client.GetAdapter()) 44 | 45 | // print the first response 46 | user := GetBatchResponseById[User](batchResponse, "1", CreateUserFromDiscriminatorValue) // returns a serialized response 47 | fmt.Println(user.GetDisplayName()) // Print display name 48 | ``` -------------------------------------------------------------------------------- /error_mappings_registry.go: -------------------------------------------------------------------------------- 1 | package msgraphgocore 2 | 3 | import ( 4 | "errors" 5 | abstractions "github.com/microsoft/kiota-abstractions-go" 6 | "sync" 7 | ) 8 | 9 | var lock = &sync.Mutex{} 10 | 11 | type errorRegistry struct { 12 | registry map[string]abstractions.ErrorMappings 13 | } 14 | 15 | var singleInstance *errorRegistry 16 | 17 | // Create a global thread safe singleton for global values 18 | func getInstance() *errorRegistry { 19 | if singleInstance == nil { 20 | lock.Lock() 21 | defer lock.Unlock() 22 | if singleInstance == nil { 23 | singleInstance = &errorRegistry{ 24 | registry: make(map[string]abstractions.ErrorMappings), 25 | } 26 | } 27 | } 28 | 29 | return singleInstance 30 | } 31 | 32 | func RegisterError(key string, value abstractions.ErrorMappings) error { 33 | single := getInstance() 34 | _, found := single.registry[key] 35 | if !found { 36 | single.registry[key] = value 37 | return nil 38 | } else { 39 | return errors.New("object Factory already register") 40 | } 41 | } 42 | 43 | func DeRegisterError(key string) error { 44 | single := getInstance() 45 | _, found := single.registry[key] 46 | if found { 47 | delete(single.registry, key) 48 | return nil 49 | } else { 50 | return errors.New("object Factory does not exist register") 51 | } 52 | } 53 | 54 | func GetErrorFactoryFromRegistry(key string) (abstractions.ErrorMappings, bool) { 55 | single := getInstance() 56 | item, found := single.registry[key] 57 | return item, found 58 | } 59 | -------------------------------------------------------------------------------- /error_mappings_registry_test.go: -------------------------------------------------------------------------------- 1 | package msgraphgocore 2 | 3 | import ( 4 | abstractions "github.com/microsoft/kiota-abstractions-go" 5 | "github.com/stretchr/testify/assert" 6 | "github.com/stretchr/testify/require" 7 | "testing" 8 | ) 9 | 10 | func TestRegistration(t *testing.T) { 11 | errorMapping := abstractions.ErrorMappings{} 12 | err := RegisterError(BatchRequestErrorRegistryKey, errorMapping) 13 | require.NoError(t, err) 14 | err = RegisterError(BatchRequestErrorRegistryKey, errorMapping) 15 | assert.Equal(t, err.Error(), "object Factory already register") 16 | 17 | err = DeRegisterError(BatchRequestErrorRegistryKey) 18 | require.NoError(t, err) 19 | err = DeRegisterError(BatchRequestErrorRegistryKey) 20 | assert.Equal(t, err.Error(), "object Factory does not exist register") 21 | } 22 | -------------------------------------------------------------------------------- /fileuploader/file_uploader_util.go: -------------------------------------------------------------------------------- 1 | package fileuploader 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | ) 7 | 8 | type rangePair struct { 9 | Start int64 10 | End int64 11 | } 12 | 13 | func stringIsNullOrEmpty(s string) bool { 14 | s = strings.TrimSpace(s) 15 | if s == "" || len(s) == 0 { 16 | return true 17 | } 18 | return false 19 | } 20 | 21 | type UploadSession interface { 22 | GetExpirationDateTime() *time.Time 23 | SetExpirationDateTime(expirationDateTime *time.Time) 24 | GetNextExpectedRanges() []string 25 | SetNextExpectedRanges(nextExpectedRanges []string) 26 | GetOdataType() *string 27 | GetUploadUrl() *string 28 | } 29 | 30 | type ProgressCallBack func(current int64, total int64) 31 | 32 | type UploadResult[T interface{}] interface { 33 | SetItemResponse(response T) 34 | GetItemResponse() T 35 | SetUploadSession(uploadSession UploadSession) 36 | GetUploadSession() UploadSession 37 | SetURI(uri *string) 38 | GetURI() *string 39 | SetUploadSucceeded(isSuccessful bool) 40 | GetUploadSucceeded() bool 41 | SetResponseErrors(errors []error) 42 | GetResponseErrors() []error 43 | } 44 | 45 | func NewUploadResult[T interface{}]() UploadResult[T] { 46 | return &uploadResult[T]{} 47 | } 48 | 49 | type uploadResult[T interface{}] struct { 50 | itemResponse T 51 | uploadSession UploadSession 52 | uri *string 53 | uploadSucceeded bool 54 | responseErrors []error 55 | } 56 | 57 | func (u *uploadResult[T]) SetItemResponse(response T) { 58 | u.itemResponse = response 59 | } 60 | 61 | func (u *uploadResult[T]) GetItemResponse() T { 62 | return u.itemResponse 63 | } 64 | 65 | func (u *uploadResult[T]) SetUploadSession(uploadSession UploadSession) { 66 | u.uploadSession = uploadSession 67 | } 68 | 69 | func (u *uploadResult[T]) GetUploadSession() UploadSession { 70 | return u.uploadSession 71 | } 72 | 73 | func (u *uploadResult[T]) SetURI(uri *string) { 74 | u.uri = uri 75 | } 76 | 77 | func (u *uploadResult[T]) GetURI() *string { 78 | return u.uri 79 | } 80 | 81 | func (u *uploadResult[T]) SetUploadSucceeded(isSuccessful bool) { 82 | u.uploadSucceeded = isSuccessful 83 | } 84 | 85 | func (u *uploadResult[T]) GetUploadSucceeded() bool { 86 | return u.uploadSucceeded 87 | } 88 | 89 | func (u *uploadResult[T]) SetResponseErrors(errors []error) { 90 | u.responseErrors = errors 91 | } 92 | 93 | func (u *uploadResult[T]) GetResponseErrors() []error { 94 | return u.responseErrors 95 | } 96 | -------------------------------------------------------------------------------- /fileuploader/large_file_session.go: -------------------------------------------------------------------------------- 1 | package fileuploader 2 | 3 | import ( 4 | "github.com/microsoft/kiota-abstractions-go/serialization" 5 | "time" 6 | ) 7 | 8 | type UploadSessionResponse interface { 9 | serialization.Parsable 10 | GetExpirationDateTime() *time.Time 11 | SetExpirationDateTime(expirationDateTime *time.Time) 12 | GetNextExpectedRanges() []string 13 | SetNextExpectedRanges(nextExpectedRanges []string) 14 | } 15 | 16 | type largeFileUploadSession struct { 17 | expirationDateTime *time.Time 18 | nextExpectedRanges []string 19 | } 20 | 21 | func (l *largeFileUploadSession) Serialize(writer serialization.SerializationWriter) error { 22 | if l.expirationDateTime != nil { 23 | if err := writer.WriteTimeValue("expirationDateTime", l.expirationDateTime); err != nil { 24 | return err 25 | } 26 | } 27 | if l.nextExpectedRanges != nil { 28 | if err := writer.WriteCollectionOfStringValues("nextExpectedRanges", l.nextExpectedRanges); err != nil { 29 | return err 30 | } 31 | } 32 | return nil 33 | } 34 | 35 | func (l *largeFileUploadSession) GetFieldDeserializers() map[string]func(serialization.ParseNode) error { 36 | return map[string]func(serialization.ParseNode) error{ 37 | "expirationDateTime": func(n serialization.ParseNode) error { 38 | val, err := n.GetTimeValue() 39 | if err != nil { 40 | return err 41 | } 42 | if val != nil { 43 | l.SetExpirationDateTime(val) 44 | } 45 | return nil 46 | }, 47 | "nextExpectedRanges": func(n serialization.ParseNode) error { 48 | val, err := n.GetCollectionOfPrimitiveValues("string") 49 | if err != nil { 50 | return err 51 | } 52 | if val != nil { 53 | res := make([]string, len(val)) 54 | for i, v := range val { 55 | if v != nil { 56 | res[i] = *(v.(*string)) 57 | } 58 | } 59 | l.SetNextExpectedRanges(res) 60 | } 61 | return nil 62 | }, 63 | } 64 | } 65 | 66 | func (l *largeFileUploadSession) GetExpirationDateTime() *time.Time { 67 | return l.expirationDateTime 68 | } 69 | 70 | func (l *largeFileUploadSession) SetExpirationDateTime(expirationDateTime *time.Time) { 71 | l.expirationDateTime = expirationDateTime 72 | } 73 | 74 | func (l *largeFileUploadSession) GetNextExpectedRanges() []string { 75 | return l.nextExpectedRanges 76 | } 77 | 78 | func (l *largeFileUploadSession) SetNextExpectedRanges(nextExpectedRanges []string) { 79 | l.nextExpectedRanges = nextExpectedRanges 80 | } 81 | 82 | func newLargeFileUploadSession() UploadSessionResponse { 83 | return &largeFileUploadSession{} 84 | } 85 | 86 | // CreateUploadSessionDiscriminator creates a new instance of the appropriate class based on discriminator value 87 | func CreateUploadSessionDiscriminator(serialization.ParseNode) (serialization.Parsable, error) { 88 | return newLargeFileUploadSession(), nil 89 | } 90 | -------------------------------------------------------------------------------- /fileuploader/large_file_upload_task.go: -------------------------------------------------------------------------------- 1 | package fileuploader 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "os" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | abstractions "github.com/microsoft/kiota-abstractions-go" 13 | "github.com/microsoft/kiota-abstractions-go/serialization" 14 | ) 15 | 16 | type LargeFileUploadTask[T serialization.Parsable] interface { 17 | Upload(progress ProgressCallBack) UploadResult[T] 18 | Resume(progress ProgressCallBack) (UploadResult[T], error) 19 | RefreshUploadStatus() error 20 | Cancel() error 21 | } 22 | 23 | // ByteStream is an interface that represents a stream of bytes 24 | type ByteStream interface { 25 | io.ReaderAt 26 | Stat() (os.FileInfo, error) 27 | } 28 | 29 | type largeFileUploadTask[T serialization.Parsable] struct { 30 | uploadSession UploadSession 31 | adapter abstractions.RequestAdapter 32 | byteStream ByteStream // *os.File by default implements ByteStream 33 | maxSlice int64 34 | parsableFactory serialization.ParsableFactory 35 | errorMappings abstractions.ErrorMappings 36 | } 37 | 38 | func NewLargeFileUploadTask[T serialization.Parsable](adapter abstractions.RequestAdapter, uploadSession UploadSession, byteStream ByteStream, maxSlice int64, parsableFactory serialization.ParsableFactory, errorMappings abstractions.ErrorMappings) LargeFileUploadTask[T] { 39 | return &largeFileUploadTask[T]{ 40 | adapter: adapter, 41 | uploadSession: uploadSession, 42 | byteStream: byteStream, 43 | maxSlice: maxSlice, 44 | parsableFactory: parsableFactory, 45 | errorMappings: errorMappings, 46 | } 47 | } 48 | 49 | // Upload uploads the byteStream in slices and returns the result of the upload 50 | func (l *largeFileUploadTask[T]) Upload(progress ProgressCallBack) UploadResult[T] { 51 | result := NewUploadResult[T]() 52 | slices := l.createUploadSlices() 53 | maxRetriesPerRequest := 3 54 | 55 | // slices of errors 56 | var responseErrors []error 57 | var itemResponse T 58 | var location *string 59 | 60 | for _, slice := range slices { 61 | response, uploadLocation, err := l.uploadWithRetry(slice, maxRetriesPerRequest) 62 | if err != nil { 63 | responseErrors = append(responseErrors, err) 64 | } else { 65 | progress(slice.RangeEnd, slice.TotalSessionLength) 66 | } 67 | if response != nil { 68 | itemResponse = response.(T) 69 | } 70 | location = uploadLocation 71 | } 72 | 73 | if len(responseErrors) > 0 { 74 | result.SetUploadSucceeded(false) 75 | result.SetResponseErrors(responseErrors) 76 | } else { 77 | result.SetUploadSucceeded(true) 78 | result.SetUploadSession(l.uploadSession) 79 | result.SetItemResponse(itemResponse) 80 | result.SetURI(location) 81 | } 82 | 83 | return result 84 | } 85 | 86 | // Resume uploads the byteStream in slices and returns the result of the upload 87 | func (l *largeFileUploadTask[T]) Resume(progress ProgressCallBack) (UploadResult[T], error) { 88 | err := l.RefreshUploadStatus() 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | if len(l.uploadSession.GetNextExpectedRanges()) == 0 { 94 | return nil, errors.New("UploadSession does not have next expected ranges") 95 | } 96 | 97 | if l.uploadSession.GetExpirationDateTime().Before(time.Now()) { 98 | return nil, errors.New("UploadSession has expired") 99 | } 100 | 101 | return l.Upload(progress), nil 102 | } 103 | 104 | func (l *largeFileUploadTask[T]) RefreshUploadStatus() error { 105 | requestInfo := abstractions.NewRequestInformation() 106 | requestInfo.UrlTemplate = *l.uploadSession.GetUploadUrl() 107 | requestInfo.Method = abstractions.GET 108 | requestInfo.Headers.TryAdd("Accept", "application/json") 109 | 110 | result, err := l.adapter.Send(context.Background(), requestInfo, CreateUploadSessionDiscriminator, l.errorMappings) 111 | if err != nil { 112 | return err 113 | } 114 | 115 | sessionResponse := result.(UploadSessionResponse) 116 | 117 | l.uploadSession.SetExpirationDateTime(sessionResponse.GetExpirationDateTime()) 118 | l.uploadSession.SetNextExpectedRanges(sessionResponse.GetNextExpectedRanges()) 119 | 120 | return nil 121 | } 122 | 123 | // Cancel cancels the upload 124 | func (l *largeFileUploadTask[T]) Cancel() error { 125 | requestInfo := abstractions.NewRequestInformationWithMethodAndUrlTemplateAndPathParameters(abstractions.DELETE, *l.uploadSession.GetUploadUrl(), make(map[string]string)) 126 | err := l.adapter.SendNoContent(context.Background(), requestInfo, l.errorMappings) 127 | return err 128 | } 129 | 130 | func (l *largeFileUploadTask[T]) uploadWithRetry(slice uploadSlice[T], maxRetry int) (interface{}, *string, error) { 131 | retry := 1 132 | var parseable interface{} 133 | var location *string 134 | var err error 135 | for retry < maxRetry { 136 | // store the result of the upload 137 | parseable, location, err = slice.Upload(l.parsableFactory) // check if successful 138 | if err != nil { 139 | if retry >= maxRetry { 140 | return nil, nil, err 141 | } 142 | // backoff before retrying 143 | time.Sleep(time.Duration(retry) * time.Second) 144 | } else { 145 | return parseable, location, err // return the result as the upload was successful 146 | } 147 | retry++ 148 | } 149 | return parseable, location, err 150 | } 151 | 152 | func (l *largeFileUploadTask[T]) getRangesRemaining() []rangePair { 153 | rangePairs := make([]rangePair, len(l.uploadSession.GetNextExpectedRanges())) 154 | 155 | for i, ranges := range l.uploadSession.GetNextExpectedRanges() { 156 | rangeValues := strings.Split(ranges, "-") 157 | 158 | var startRange int64 159 | if s, err := strconv.ParseInt(rangeValues[0], 10, 64); err == nil { 160 | startRange = s 161 | } 162 | 163 | var endRange int64 164 | if !stringIsNullOrEmpty(rangeValues[1]) { 165 | if s, err := strconv.ParseInt(rangeValues[1], 10, 64); err == nil { 166 | if endRange > l.fileSize() { 167 | endRange = l.fileSize() - 1 168 | } else { 169 | endRange = s 170 | } 171 | } 172 | } else { 173 | endRange = l.fileSize() - 1 174 | } 175 | 176 | rangePairs[i] = rangePair{ 177 | Start: startRange, 178 | End: endRange, 179 | } 180 | } 181 | 182 | return rangePairs 183 | } 184 | 185 | // returns the size of a byteStream 186 | func (l *largeFileUploadTask[T]) fileSize() int64 { 187 | fileInfo, _ := l.byteStream.Stat() 188 | return fileInfo.Size() 189 | } 190 | -------------------------------------------------------------------------------- /fileuploader/large_file_upload_test.go: -------------------------------------------------------------------------------- 1 | package fileuploader 2 | 3 | import ( 4 | "fmt" 5 | abstractions "github.com/microsoft/kiota-abstractions-go" 6 | "github.com/microsoft/kiota-abstractions-go/authentication" 7 | absser "github.com/microsoft/kiota-abstractions-go/serialization" 8 | jsonserialization "github.com/microsoft/kiota-serialization-json-go" 9 | msgraphgocore "github.com/microsoftgraph/msgraph-sdk-go-core" 10 | "github.com/microsoftgraph/msgraph-sdk-go-core/internal" 11 | "github.com/stretchr/testify/assert" 12 | "net/http" 13 | "net/http/httptest" 14 | "testing" 15 | "time" 16 | ) 17 | 18 | func prepareUploader(testServer *httptest.Server) LargeFileUploadTask[internal.UploadResponseble] { 19 | absser.DefaultParseNodeFactoryInstance.ContentTypeAssociatedFactories["application/json"] = jsonserialization.NewJsonParseNodeFactory() 20 | 21 | reqAdapter, _ := msgraphgocore.NewGraphRequestAdapterBase(&authentication.AnonymousAuthenticationProvider{}, msgraphgocore.GraphClientOptions{ 22 | GraphServiceVersion: "", 23 | GraphServiceLibraryVersion: "", 24 | }) 25 | 26 | mockPath := testServer.URL + "/uploadUrl" 27 | reqAdapter.SetBaseUrl(mockPath) 28 | 29 | byteStream := &internal.MockByteStream{ 30 | Content: []byte("mock byteStream content"), 31 | } 32 | 33 | uploadSession := &mockUploadSession{ 34 | UploadUrl: mockPath, 35 | ExpectedRanges: []string{"0-4", "6-"}, 36 | OdataType: "odatatype", 37 | ExpirationDateTime: time.Time{}, 38 | } 39 | maxSliceSize := 2 40 | 41 | errorMapping := abstractions.ErrorMappings{ 42 | "4XX": internal.CreateSampleErrorFromDiscriminatorValue, 43 | "5XX": internal.CreateSampleErrorFromDiscriminatorValue, 44 | } 45 | 46 | return NewLargeFileUploadTask[internal.UploadResponseble](reqAdapter, uploadSession, byteStream, int64(maxSliceSize), internal.CreateUploadResponseFromDiscriminatorValue, errorMapping) 47 | } 48 | 49 | func TestLargeFileUploadTask(t *testing.T) { 50 | testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 51 | w.Header().Set("Content-Type", "application/json") 52 | jsonResponse := `{ 53 | "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#microsoft.graph.uploadSession", 54 | "uploadUrl": "https://uploadUrl", 55 | "expirationDateTime": "2021-08-10T00:00:00Z" 56 | }` 57 | w.WriteHeader(200) 58 | fmt.Fprint(w, jsonResponse) 59 | })) 60 | defer testServer.Close() 61 | 62 | uploader := prepareUploader(testServer) 63 | 64 | // verify that the object was created correctly 65 | // verify the number of sub upload tasks 66 | progressCall := 0 67 | var previousValue int64 = 0 68 | progress := func(progress int64, total int64) { 69 | progressCall++ 70 | if previousValue > progress { 71 | assert.Fail(t, "progress should not decrease") 72 | } 73 | previousValue = progress 74 | } 75 | result := uploader.Upload(progress) 76 | 77 | // verify that status is correct 78 | assert.True(t, result.GetUploadSucceeded()) 79 | assert.Equal(t, 12, progressCall) // progress callback should be called for every sub upload task 80 | } 81 | 82 | func TestResumeLargeFileUploadTask(t *testing.T) { 83 | testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 84 | w.Header().Set("Content-Type", "application/json") 85 | 86 | testTime := time.Now().Add(1 * time.Hour).Format("2006-01-02T15:04:05Z") 87 | jsonResponse := `{ 88 | "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#microsoft.graph.uploadSession", 89 | "uploadUrl": "https://uploadUrl", 90 | "expirationDateTime": "%s", 91 | "nextExpectedRanges": ["0-4", "6-"] 92 | }` 93 | w.WriteHeader(200) 94 | formattedResponse := fmt.Sprintf(jsonResponse, testTime) 95 | fmt.Fprint(w, formattedResponse) 96 | })) 97 | defer testServer.Close() 98 | 99 | uploader := prepareUploader(testServer) 100 | 101 | progressCall := 0 102 | progress := func(progress int64, total int64) { 103 | progressCall++ 104 | } 105 | result, err := uploader.Resume(progress) 106 | assert.NoError(t, err) 107 | 108 | // verify that status is correct 109 | assert.True(t, result.GetUploadSucceeded()) 110 | assert.Equal(t, 12, progressCall) // progress callback should be called for every sub upload task 111 | 112 | } 113 | 114 | func TestCancelLargeFileUploadTask(t *testing.T) { 115 | 116 | var receivedReq *http.Request 117 | testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 118 | w.Header().Set("Content-Type", "application/json") 119 | w.WriteHeader(204) 120 | receivedReq = req 121 | })) 122 | defer testServer.Close() 123 | 124 | uploader := prepareUploader(testServer) 125 | err := uploader.Cancel() 126 | assert.NoError(t, err) 127 | assert.Equal(t, "DELETE", receivedReq.Method) 128 | } 129 | 130 | type mockUploadSession struct { 131 | UploadUrl string 132 | ExpectedRanges []string 133 | OdataType string 134 | ExpirationDateTime time.Time 135 | } 136 | 137 | func (m *mockUploadSession) SetExpirationDateTime(expirationDateTime *time.Time) { 138 | m.ExpirationDateTime = *expirationDateTime 139 | } 140 | 141 | func (m *mockUploadSession) SetNextExpectedRanges(nextExpectedRanges []string) { 142 | m.ExpectedRanges = nextExpectedRanges 143 | } 144 | 145 | func (m *mockUploadSession) Serialize(writer absser.SerializationWriter) error { 146 | return nil 147 | } 148 | 149 | func (m *mockUploadSession) GetFieldDeserializers() map[string]func(absser.ParseNode) error { 150 | return make(map[string]func(absser.ParseNode) error) 151 | } 152 | 153 | func (m *mockUploadSession) GetExpirationDateTime() *time.Time { 154 | return &m.ExpirationDateTime 155 | } 156 | 157 | func (m *mockUploadSession) GetNextExpectedRanges() []string { 158 | return m.ExpectedRanges 159 | } 160 | 161 | func (m *mockUploadSession) GetOdataType() *string { 162 | return &m.OdataType 163 | } 164 | 165 | func (m *mockUploadSession) GetUploadUrl() *string { 166 | return &m.UploadUrl 167 | } 168 | -------------------------------------------------------------------------------- /fileuploader/upload_slice.go: -------------------------------------------------------------------------------- 1 | package fileuploader 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | abstractions "github.com/microsoft/kiota-abstractions-go" 7 | "github.com/microsoft/kiota-abstractions-go/serialization" 8 | nethttplibrary "github.com/microsoft/kiota-http-go" 9 | "time" 10 | ) 11 | 12 | const binaryContentType = "application/octet-stream" 13 | 14 | const uriLocationHeader = "Location" 15 | 16 | type uploadSlice[T serialization.Parsable] struct { 17 | RequestAdapter abstractions.RequestAdapter 18 | UrlTemplate string 19 | RangeBegin int64 20 | RangeEnd int64 21 | TotalSessionLength int64 22 | RangeLength int64 23 | byteStream ByteStream 24 | errorMappings abstractions.ErrorMappings 25 | } 26 | 27 | func (l *largeFileUploadTask[T]) createUploadSlices() []uploadSlice[T] { 28 | 29 | requestRanges := l.getRangesRemaining() 30 | maxSlice := l.maxSlice 31 | totalSessionLength := l.fileSize() 32 | 33 | // compute the correct upload ranges by splitting the values of ranges remaining from start to end 34 | var uploadSlices []uploadSlice[T] 35 | for _, v := range requestRanges { 36 | start := v.Start 37 | for start < totalSessionLength && start <= v.End { 38 | end := minOf(v.End, (start+maxSlice)-1, totalSessionLength-1) 39 | uploadSlices = append(uploadSlices, uploadSlice[T]{ 40 | RequestAdapter: l.adapter, 41 | UrlTemplate: *l.uploadSession.GetUploadUrl(), 42 | RangeBegin: start, 43 | RangeEnd: end, 44 | RangeLength: end - start + 1, 45 | TotalSessionLength: totalSessionLength, 46 | errorMappings: l.errorMappings, 47 | byteStream: l.byteStream, 48 | }) 49 | start = end + 1 50 | } 51 | } 52 | 53 | return uploadSlices 54 | } 55 | 56 | func minOf(vars ...int64) int64 { 57 | minimum := vars[0] 58 | for _, i := range vars { 59 | if minimum > i { 60 | minimum = i 61 | } 62 | } 63 | return minimum 64 | } 65 | 66 | func (u *uploadSlice[T]) Upload(parsableFactory serialization.ParsableFactory) (interface{}, *string, error) { 67 | data, err := u.readSection(u.RangeBegin, u.RangeEnd) 68 | if err != nil { 69 | return nil, nil, err 70 | } 71 | requestInfo := u.createRequestInformation(data) 72 | 73 | // limit the upload time per slice to 5 minutes 74 | ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) 75 | defer cancel() 76 | 77 | headerOptions := nethttplibrary.NewHeadersInspectionOptions() 78 | headerOptions.InspectResponseHeaders = true 79 | requestInfo.AddRequestOptions([]abstractions.RequestOption{headerOptions}) 80 | 81 | result, err := u.RequestAdapter.Send(ctx, requestInfo, parsableFactory, u.errorMappings) 82 | 83 | var location *string = nil 84 | locations := headerOptions.GetResponseHeaders().Get(uriLocationHeader) 85 | if len(locations) > 0 { 86 | location = &locations[0] 87 | } 88 | 89 | return result, location, err 90 | } 91 | 92 | func (u *uploadSlice[T]) readSection(start, end int64) ([]byte, error) { 93 | length := (end - start) + 1 94 | 95 | buffer := make([]byte, length) 96 | _, err := u.byteStream.ReadAt(buffer, start) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | return buffer, nil 102 | } 103 | 104 | func (u *uploadSlice[T]) createRequestInformation(content []byte) *abstractions.RequestInformation { 105 | headers := abstractions.NewRequestHeaders() 106 | headers.Add("Content-Range", fmt.Sprintf("bytes %d-%d/%d", u.RangeBegin, u.RangeEnd, u.TotalSessionLength)) 107 | headers.Add("Content-Length", fmt.Sprintf("%d", u.RangeLength)) 108 | 109 | requestInfo := abstractions.NewRequestInformation() 110 | requestInfo.Headers = headers 111 | requestInfo.UrlTemplate = u.UrlTemplate 112 | requestInfo.Method = abstractions.PUT 113 | requestInfo.SetStreamContentAndContentType(content, binaryContentType) 114 | return requestInfo 115 | } 116 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/microsoftgraph/msgraph-sdk-go-core 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 9 | github.com/google/uuid v1.6.0 10 | github.com/microsoft/kiota-abstractions-go v1.9.2 11 | github.com/microsoft/kiota-authentication-azure-go v1.3.0 12 | github.com/microsoft/kiota-http-go v1.5.4 13 | github.com/microsoft/kiota-serialization-json-go v1.1.2 14 | github.com/stretchr/testify v1.10.0 15 | ) 16 | 17 | require ( 18 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.0 // indirect 19 | github.com/davecgh/go-spew v1.1.1 // indirect 20 | github.com/go-logr/logr v1.4.2 // indirect 21 | github.com/go-logr/stdr v1.2.2 // indirect 22 | github.com/pmezard/go-difflib v1.0.0 // indirect 23 | github.com/std-uritemplate/std-uritemplate/go/v2 v2.0.3 // indirect 24 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 25 | go.opentelemetry.io/otel v1.35.0 // indirect 26 | go.opentelemetry.io/otel/metric v1.35.0 // indirect 27 | go.opentelemetry.io/otel/trace v1.35.0 // indirect 28 | golang.org/x/net v0.38.0 // indirect 29 | golang.org/x/text v0.23.0 // indirect 30 | gopkg.in/yaml.v3 v3.0.1 // indirect 31 | ) 32 | 33 | retract ( 34 | v0.11.0 35 | // error in version bump, bumped minor instead of patch, causing issues with update commands as long as we don't have a higher version number 36 | v0.0.14 37 | // contains retraction only 38 | ) 39 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= 2 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= 3 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.0 h1:Bg8m3nq/X1DeePkAbCfb6ml6F3F0IunEhE8TMh+lY48= 4 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.0/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 8 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 9 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 10 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 11 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 12 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 13 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 14 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 15 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 16 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 17 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 18 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 19 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 20 | github.com/microsoft/kiota-abstractions-go v1.9.2 h1:3U5VgN2YGe3lsu1pyuS0t5jxv1llxX2ophwX8ewE6wQ= 21 | github.com/microsoft/kiota-abstractions-go v1.9.2/go.mod h1:f06pl3qSyvUHEfVNkiRpXPkafx7khZqQEb71hN/pmuU= 22 | github.com/microsoft/kiota-authentication-azure-go v1.3.0 h1:PWH6PgtzhJjnmvR6N1CFjriwX09Kv7S5K3vL6VbPVrg= 23 | github.com/microsoft/kiota-authentication-azure-go v1.3.0/go.mod h1:l/MPGUVvD7xfQ+MYSdZaFPv0CsLDqgSOp8mXwVgArIs= 24 | github.com/microsoft/kiota-http-go v1.5.4 h1:wSUmL1J+bTQlAWHjbRkSwr+SPAkMVYeYxxB85Zw0KFs= 25 | github.com/microsoft/kiota-http-go v1.5.4/go.mod h1:L+5Ri+SzwELnUcNA0cpbFKp/pBbvypLh3Cd1PR6sjx0= 26 | github.com/microsoft/kiota-serialization-json-go v1.1.2 h1:eJrPWeQ665nbjO0gsHWJ0Bw6V/ZHHU1OfFPaYfRG39k= 27 | github.com/microsoft/kiota-serialization-json-go v1.1.2/go.mod h1:deaGt7fjZarywyp7TOTiRsjfYiyWxwJJPQZytXwYQn8= 28 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 29 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 30 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 31 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 32 | github.com/std-uritemplate/std-uritemplate/go/v2 v2.0.3 h1:7hth9376EoQEd1hH4lAp3vnaLP2UMyxuMMghLKzDHyU= 33 | github.com/std-uritemplate/std-uritemplate/go/v2 v2.0.3/go.mod h1:Z5KcoM0YLC7INlNhEezeIZ0TZNYf7WSNO0Lvah4DSeQ= 34 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 35 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 36 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 37 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 38 | go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= 39 | go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= 40 | go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= 41 | go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= 42 | go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= 43 | go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= 44 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 45 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 46 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 47 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 48 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 49 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 50 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 51 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 52 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 53 | -------------------------------------------------------------------------------- /graph_client_factory.go: -------------------------------------------------------------------------------- 1 | package msgraphgocore 2 | 3 | import ( 4 | nethttp "net/http" 5 | 6 | khttp "github.com/microsoft/kiota-http-go" 7 | ) 8 | 9 | var ReplacementPairs = map[string]string{"/users/me-token-to-replace": "/me"} 10 | 11 | // GetDefaultMiddlewaresWithOptions creates a default slice of middleware for the Graph Client. 12 | func GetDefaultMiddlewaresWithOptions(options *GraphClientOptions) []khttp.Middleware { 13 | kiotaMiddlewares := khttp.GetDefaultMiddlewares() 14 | graphMiddlewares := []khttp.Middleware{ 15 | NewGraphTelemetryHandler(options), 16 | khttp.NewUrlReplaceHandler(true, ReplacementPairs), 17 | } 18 | graphMiddlewaresLen := len(graphMiddlewares) 19 | resultMiddlewares := make([]khttp.Middleware, len(kiotaMiddlewares)+graphMiddlewaresLen) 20 | copy(resultMiddlewares, graphMiddlewares) 21 | copy(resultMiddlewares[graphMiddlewaresLen:], kiotaMiddlewares) 22 | return resultMiddlewares 23 | } 24 | 25 | // GetDefaultClient creates a new http client with a preconfigured middleware pipeline 26 | func GetDefaultClient(options *GraphClientOptions, middleware ...khttp.Middleware) *nethttp.Client { 27 | if len(middleware) == 0 { 28 | middleware = GetDefaultMiddlewaresWithOptions(options) 29 | } 30 | return khttp.GetDefaultClient(middleware...) 31 | } 32 | -------------------------------------------------------------------------------- /graph_client_options.go: -------------------------------------------------------------------------------- 1 | package msgraphgocore 2 | 3 | // GraphClientOptions represents a combination of GraphServiceVersion and GraphServiceLibraryVersion 4 | // 5 | // GraphServiceVersion is version of the targeted service. 6 | // GraphServiceLibraryVersion is the version of the service library 7 | type GraphClientOptions struct { 8 | GraphServiceVersion string 9 | GraphServiceLibraryVersion string 10 | } 11 | -------------------------------------------------------------------------------- /graph_request_adapter_base.go: -------------------------------------------------------------------------------- 1 | package msgraphgocore 2 | 3 | import ( 4 | "errors" 5 | nethttp "net/http" 6 | 7 | absauth "github.com/microsoft/kiota-abstractions-go/authentication" 8 | absser "github.com/microsoft/kiota-abstractions-go/serialization" 9 | khttp "github.com/microsoft/kiota-http-go" 10 | ) 11 | 12 | // GraphRequestAdapterBase is the core service used by GraphServiceClient to make requests to Microsoft Graph. 13 | type GraphRequestAdapterBase struct { 14 | khttp.NetHttpRequestAdapter 15 | } 16 | 17 | // NewGraphRequestAdapterBase creates a new GraphRequestAdapterBase with the given parameters 18 | func NewGraphRequestAdapterBase(authenticationProvider absauth.AuthenticationProvider, clientOptions GraphClientOptions) (*GraphRequestAdapterBase, error) { 19 | return NewGraphRequestAdapterBaseWithParseNodeFactory(authenticationProvider, clientOptions, nil) 20 | } 21 | 22 | // NewGraphRequestAdapterBaseWithParseNodeFactory creates a new GraphRequestAdapterBase with the given parameters 23 | func NewGraphRequestAdapterBaseWithParseNodeFactory(authenticationProvider absauth.AuthenticationProvider, clientOptions GraphClientOptions, parseNodeFactory absser.ParseNodeFactory) (*GraphRequestAdapterBase, error) { 24 | return NewGraphRequestAdapterBaseWithParseNodeFactoryAndSerializationWriterFactory(authenticationProvider, clientOptions, parseNodeFactory, nil) 25 | } 26 | 27 | // NewGraphRequestAdapterBaseWithParseNodeFactoryAndSerializationWriterFactory creates a new GraphRequestAdapterBase with the given parameters 28 | func NewGraphRequestAdapterBaseWithParseNodeFactoryAndSerializationWriterFactory(authenticationProvider absauth.AuthenticationProvider, clientOptions GraphClientOptions, parseNodeFactory absser.ParseNodeFactory, serializationWriterFactory absser.SerializationWriterFactory) (*GraphRequestAdapterBase, error) { 29 | return NewGraphRequestAdapterBaseWithParseNodeFactoryAndSerializationWriterFactoryAndHttpClient(authenticationProvider, clientOptions, parseNodeFactory, serializationWriterFactory, nil) 30 | } 31 | 32 | // NewGraphRequestAdapterBaseWithParseNodeFactoryAndSerializationWriterFactoryAndHttpClient creates a new GraphRequestAdapterBase with the given parameters 33 | func NewGraphRequestAdapterBaseWithParseNodeFactoryAndSerializationWriterFactoryAndHttpClient(authenticationProvider absauth.AuthenticationProvider, clientOptions GraphClientOptions, parseNodeFactory absser.ParseNodeFactory, serializationWriterFactory absser.SerializationWriterFactory, httpClient *nethttp.Client) (*GraphRequestAdapterBase, error) { 34 | if authenticationProvider == nil { 35 | return nil, errors.New("authenticationProvider cannot be nil") 36 | } 37 | if httpClient == nil { 38 | httpClient = GetDefaultClient(&clientOptions) 39 | } 40 | if serializationWriterFactory == nil { 41 | serializationWriterFactory = absser.DefaultSerializationWriterFactoryInstance 42 | } 43 | if parseNodeFactory == nil { 44 | parseNodeFactory = absser.DefaultParseNodeFactoryInstance 45 | } 46 | baseAdapter, err := khttp.NewNetHttpRequestAdapterWithParseNodeFactoryAndSerializationWriterFactoryAndHttpClient(authenticationProvider, parseNodeFactory, serializationWriterFactory, httpClient) 47 | if err != nil { 48 | return nil, err 49 | } 50 | result := &GraphRequestAdapterBase{ 51 | NetHttpRequestAdapter: *baseAdapter, 52 | } 53 | 54 | return result, nil 55 | } 56 | -------------------------------------------------------------------------------- /graph_telemetry_handler.go: -------------------------------------------------------------------------------- 1 | package msgraphgocore 2 | 3 | import ( 4 | nethttp "net/http" 5 | 6 | runtime "runtime" 7 | 8 | uuid "github.com/google/uuid" 9 | khttp "github.com/microsoft/kiota-http-go" 10 | ) 11 | 12 | // GraphTelemetryHandler is a middleware handler that adds telemetry headers to requests. 13 | type GraphTelemetryHandler struct { 14 | sdkVersion string 15 | } 16 | 17 | // NewGraphTelemetryHandler creates a new GraphTelemetryHandler. 18 | func NewGraphTelemetryHandler(options *GraphClientOptions) *GraphTelemetryHandler { 19 | serviceVersionPrefix := "" 20 | if options != nil && options.GraphServiceLibraryVersion != "" { 21 | serviceVersionPrefix += "graph-go" 22 | if options.GraphServiceVersion != "" { 23 | serviceVersionPrefix += "-" + options.GraphServiceVersion 24 | } 25 | serviceVersionPrefix += "/" + options.GraphServiceLibraryVersion 26 | serviceVersionPrefix += ", " 27 | } 28 | featuresSuffix := "" 29 | if runtime.GOOS != "" { 30 | featuresSuffix += " hostOS=" + runtime.GOOS + ";" 31 | } 32 | if runtime.GOARCH != "" { 33 | featuresSuffix += " hostArch=" + runtime.GOARCH + ";" 34 | } 35 | goVersion := runtime.Version() 36 | if goVersion != "" { 37 | featuresSuffix += " runtimeEnvironment=" + goVersion + ";" 38 | } 39 | if featuresSuffix != "" { 40 | featuresSuffix = " (" + featuresSuffix[1:] + ")" 41 | } 42 | return &GraphTelemetryHandler{ 43 | sdkVersion: serviceVersionPrefix + "graph-go-core/" + CoreVersion + featuresSuffix, 44 | } 45 | } 46 | func (middleware GraphTelemetryHandler) Intercept(pipeline khttp.Pipeline, middlewareIndex int, req *nethttp.Request) (*nethttp.Response, error) { 47 | req.Header.Add("SdkVersion", middleware.sdkVersion) 48 | req.Header.Add("client-request-id", uuid.NewString()) 49 | return pipeline.Next(req, middlewareIndex) 50 | } 51 | -------------------------------------------------------------------------------- /graph_telemetry_handler_test.go: -------------------------------------------------------------------------------- 1 | package msgraphgocore 2 | 3 | import ( 4 | nethttp "net/http" 5 | httptest "net/http/httptest" 6 | testing "testing" 7 | 8 | assert "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | type NoopPipeline struct { 12 | client *nethttp.Client 13 | } 14 | 15 | func (pipeline *NoopPipeline) Next(req *nethttp.Request, middlewareIndex int) (*nethttp.Response, error) { 16 | return pipeline.client.Do(req) 17 | } 18 | func newNoopPipeline() *NoopPipeline { 19 | return &NoopPipeline{ 20 | client: nethttp.DefaultClient, 21 | } 22 | } 23 | 24 | func TestItCreatesANewHandler(t *testing.T) { 25 | handler := NewGraphTelemetryHandler(&GraphClientOptions{}) 26 | if handler == nil { 27 | t.Error("handler is nil") 28 | } 29 | } 30 | 31 | func TestItAddsHeaders(t *testing.T) { 32 | testServer := httptest.NewServer(nethttp.HandlerFunc(func(res nethttp.ResponseWriter, req *nethttp.Request) { 33 | res.WriteHeader(200) 34 | res.Write([]byte("body")) 35 | })) 36 | defer func() { testServer.Close() }() 37 | handler := NewGraphTelemetryHandler(&GraphClientOptions{}) 38 | req, err := nethttp.NewRequest(nethttp.MethodGet, testServer.URL, nil) 39 | if err != nil { 40 | t.Error(err) 41 | } 42 | resp, err := handler.Intercept(newNoopPipeline(), 0, req) 43 | if err != nil { 44 | t.Error(err) 45 | } 46 | assert.NotNil(t, resp) 47 | sdkVersionHeaderValue := req.Header[nethttp.CanonicalHeaderKey("SdkVersion")] 48 | assert.NotEmpty(t, sdkVersionHeaderValue) 49 | assert.Contains(t, sdkVersionHeaderValue[0], "graph-go-core") 50 | assert.Contains(t, sdkVersionHeaderValue[0], "hostOS") 51 | assert.Contains(t, sdkVersionHeaderValue[0], "hostArch") 52 | assert.Contains(t, sdkVersionHeaderValue[0], "runtimeEnvironment") 53 | assert.NotEmpty(t, req.Header[nethttp.CanonicalHeaderKey("client-request-id")]) 54 | } 55 | 56 | func TestItAddsServiceLibInfo(t *testing.T) { 57 | testServer := httptest.NewServer(nethttp.HandlerFunc(func(res nethttp.ResponseWriter, req *nethttp.Request) { 58 | res.WriteHeader(200) 59 | res.Write([]byte("body")) 60 | })) 61 | defer func() { testServer.Close() }() 62 | handler := NewGraphTelemetryHandler(&GraphClientOptions{ 63 | GraphServiceLibraryVersion: "1.0.0", 64 | }) 65 | req, err := nethttp.NewRequest(nethttp.MethodGet, testServer.URL, nil) 66 | if err != nil { 67 | t.Error(err) 68 | } 69 | resp, err := handler.Intercept(newNoopPipeline(), 0, req) 70 | if err != nil { 71 | t.Error(err) 72 | } 73 | assert.NotNil(t, resp) 74 | sdkVersionHeaderValue := req.Header[nethttp.CanonicalHeaderKey("SdkVersion")] 75 | assert.NotEmpty(t, sdkVersionHeaderValue) 76 | assert.Contains(t, sdkVersionHeaderValue[0], "graph-go/") 77 | } 78 | 79 | func TestItAddsServiceInfo(t *testing.T) { 80 | testServer := httptest.NewServer(nethttp.HandlerFunc(func(res nethttp.ResponseWriter, req *nethttp.Request) { 81 | res.WriteHeader(200) 82 | res.Write([]byte("body")) 83 | })) 84 | defer func() { testServer.Close() }() 85 | handler := NewGraphTelemetryHandler(&GraphClientOptions{ 86 | GraphServiceLibraryVersion: "1.0.0", 87 | GraphServiceVersion: "v1", 88 | }) 89 | req, err := nethttp.NewRequest(nethttp.MethodGet, testServer.URL, nil) 90 | if err != nil { 91 | t.Error(err) 92 | } 93 | resp, err := handler.Intercept(newNoopPipeline(), 0, req) 94 | if err != nil { 95 | t.Error(err) 96 | } 97 | assert.NotNil(t, resp) 98 | sdkVersionHeaderValue := req.Header[nethttp.CanonicalHeaderKey("SdkVersion")] 99 | assert.NotEmpty(t, sdkVersionHeaderValue) 100 | assert.Contains(t, sdkVersionHeaderValue[0], "graph-go-v1/") 101 | } 102 | -------------------------------------------------------------------------------- /internal/errors.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | abstractions "github.com/microsoft/kiota-abstractions-go" 5 | "github.com/microsoft/kiota-abstractions-go/serialization" 6 | ) 7 | 8 | type SampleError struct { 9 | abstractions.ApiError 10 | // Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. 11 | additionalData map[string]interface{} 12 | } 13 | 14 | func (s SampleError) Serialize(writer serialization.SerializationWriter) error { 15 | return nil 16 | } 17 | 18 | func (s *SampleError) GetFieldDeserializers() map[string]func(serialization.ParseNode) error { 19 | res := make(map[string]func(serialization.ParseNode) error) 20 | res["error"] = func(n serialization.ParseNode) error { 21 | v, err := n.GetRawValue() 22 | if err != nil { 23 | return err 24 | } 25 | if vm, ok := v.(map[string]interface{}); ok { 26 | if msg, ok := vm["message"]; ok && msg != nil { 27 | s.Message = *msg.(*string) 28 | } 29 | } 30 | return nil 31 | } 32 | return res 33 | } 34 | 35 | func CreateSampleErrorFromDiscriminatorValue(parseNode serialization.ParseNode) (serialization.Parsable, error) { 36 | res := SampleError{} 37 | return &res, nil 38 | } 39 | -------------------------------------------------------------------------------- /internal/invalid_user_response.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | // InvalidUsersResponse 4 | type InvalidUsersResponse struct { 5 | // Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. 6 | additionalData map[string]interface{} 7 | // 8 | nextLink *string 9 | // 10 | value []User 11 | } 12 | 13 | // NewInvalidUsersResponse instantiates a new InvalidUsersResponse and sets the default values. 14 | func NewInvalidUsersResponse() *InvalidUsersResponse { 15 | m := &InvalidUsersResponse{} 16 | m.SetAdditionalData(make(map[string]interface{})) 17 | return m 18 | } 19 | 20 | // GetAdditionalData gets the additionalData property value. Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. 21 | func (m *InvalidUsersResponse) GetAdditionalData() map[string]interface{} { 22 | if m == nil { 23 | return nil 24 | } else { 25 | return m.additionalData 26 | } 27 | } 28 | 29 | // GetNextLink gets the @odata.nextLink property value. 30 | func (m *InvalidUsersResponse) GetNextLink() *string { 31 | if m == nil { 32 | return nil 33 | } else { 34 | return m.nextLink 35 | } 36 | } 37 | 38 | // GetValue gets the value property value. 39 | func (m *InvalidUsersResponse) GetValue() []User { 40 | if m == nil { 41 | return nil 42 | } else { 43 | return m.value 44 | } 45 | } 46 | 47 | func (m *InvalidUsersResponse) IsNil() bool { 48 | return m == nil 49 | } 50 | 51 | // SetAdditionalData sets the additionalData property value. Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. 52 | func (m *InvalidUsersResponse) SetAdditionalData(value map[string]interface{}) { 53 | if m != nil { 54 | m.additionalData = value 55 | } 56 | } 57 | 58 | // SetNextLink sets the @odata.nextLink property value. 59 | func (m *InvalidUsersResponse) SetNextLink(value *string) { 60 | if m != nil { 61 | m.nextLink = value 62 | } 63 | } 64 | 65 | // SetValue sets the value property value. 66 | func (m *InvalidUsersResponse) SetValue(value []User) { 67 | if m != nil { 68 | m.value = value 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /internal/test_byte_stream.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/microsoft/kiota-abstractions-go/serialization" 5 | "io/fs" 6 | "os" 7 | "time" 8 | ) 9 | 10 | type MockByteStream struct { 11 | Content []byte 12 | } 13 | 14 | func (m *MockByteStream) ReadAt(p []byte, off int64) (n int, err error) { 15 | v := copy(p, m.Content[off:]) 16 | return v, nil 17 | } 18 | 19 | func (m *MockByteStream) Stat() (os.FileInfo, error) { 20 | return &fakeFileInfo{ 21 | dir: false, 22 | basename: "mockByteStream", 23 | modtime: time.Time{}, 24 | ents: nil, 25 | contents: string(m.Content), 26 | err: nil, 27 | }, nil 28 | } 29 | 30 | type fakeFileInfo struct { 31 | dir bool 32 | basename string 33 | modtime time.Time 34 | ents []*fakeFileInfo 35 | contents string 36 | err error 37 | } 38 | 39 | func (f *fakeFileInfo) Name() string { return f.basename } 40 | func (f *fakeFileInfo) Sys() any { return nil } 41 | func (f *fakeFileInfo) ModTime() time.Time { return f.modtime } 42 | func (f *fakeFileInfo) IsDir() bool { return f.dir } 43 | func (f *fakeFileInfo) Size() int64 { return int64(len(f.contents)) } 44 | func (f *fakeFileInfo) Mode() fs.FileMode { 45 | if f.dir { 46 | return 0755 | fs.ModeDir 47 | } 48 | return 0644 49 | } 50 | 51 | type UploadResponse struct { 52 | // Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. 53 | additionalData map[string]interface{} 54 | } 55 | 56 | func (s *UploadResponse) Serialize(writer serialization.SerializationWriter) error { 57 | return nil 58 | } 59 | 60 | func (s *UploadResponse) GetFieldDeserializers() map[string]func(serialization.ParseNode) error { 61 | return make(map[string]func(serialization.ParseNode) error) 62 | } 63 | 64 | func CreateUploadResponseFromDiscriminatorValue(parseNode serialization.ParseNode) (serialization.Parsable, error) { 65 | res := UploadResponse{} 66 | return &res, nil 67 | } 68 | 69 | type UploadResponseble interface { 70 | Serialize(writer serialization.SerializationWriter) error 71 | GetFieldDeserializers() map[string]func(serialization.ParseNode) error 72 | } 73 | -------------------------------------------------------------------------------- /internal/user.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | i336074805fc853987abe6f7fe3ad97a6a6f3077a16391fec744f671a015fbd7e "time" 5 | 6 | i04eb5309aeaafadd28374d79c8471df9b267510b4dc2e3144c378c50f6fd7b55 "github.com/microsoft/kiota-abstractions-go/serialization" 7 | ) 8 | 9 | type User struct { 10 | DisplayName *string 11 | DirectoryObject 12 | } 13 | 14 | func (u *User) GetDisplayName() *string { 15 | return u.DisplayName 16 | } 17 | 18 | var displayName = "A User" 19 | 20 | func NewUser() *User { 21 | return &User{ 22 | DisplayName: &displayName, 23 | } 24 | } 25 | 26 | type DirectoryObject struct { 27 | Entity 28 | // 29 | deletedDateTime *i336074805fc853987abe6f7fe3ad97a6a6f3077a16391fec744f671a015fbd7e.Time 30 | } 31 | 32 | // NewDirectoryObject instantiates a new directoryObject and sets the default values. 33 | func NewDirectoryObject() *DirectoryObject { 34 | m := &DirectoryObject{ 35 | Entity: *NewEntity(), 36 | } 37 | return m 38 | } 39 | 40 | // GetDeletedDateTime gets the deletedDateTime property value. 41 | func (m *DirectoryObject) GetDeletedDateTime() *i336074805fc853987abe6f7fe3ad97a6a6f3077a16391fec744f671a015fbd7e.Time { 42 | if m == nil { 43 | return nil 44 | } else { 45 | return m.deletedDateTime 46 | } 47 | } 48 | 49 | // GetFieldDeserializers the deserialization information for the current model 50 | func (m *DirectoryObject) GetFieldDeserializers() map[string]func(i04eb5309aeaafadd28374d79c8471df9b267510b4dc2e3144c378c50f6fd7b55.ParseNode) error { 51 | res := m.Entity.GetFieldDeserializers() 52 | res["deletedDateTime"] = func(n i04eb5309aeaafadd28374d79c8471df9b267510b4dc2e3144c378c50f6fd7b55.ParseNode) error { 53 | val, err := n.GetTimeValue() 54 | if err != nil { 55 | return err 56 | } 57 | if val != nil { 58 | m.SetDeletedDateTime(val) 59 | } 60 | return nil 61 | } 62 | return res 63 | } 64 | func (m *DirectoryObject) IsNil() bool { 65 | return m == nil 66 | } 67 | 68 | // Serialize serializes information the current object 69 | func (m *DirectoryObject) Serialize(writer i04eb5309aeaafadd28374d79c8471df9b267510b4dc2e3144c378c50f6fd7b55.SerializationWriter) error { 70 | err := m.Entity.Serialize(writer) 71 | if err != nil { 72 | return err 73 | } 74 | { 75 | err = writer.WriteTimeValue("deletedDateTime", m.GetDeletedDateTime()) 76 | if err != nil { 77 | return err 78 | } 79 | } 80 | return nil 81 | } 82 | 83 | // SetDeletedDateTime sets the deletedDateTime property value. 84 | func (m *DirectoryObject) SetDeletedDateTime(value *i336074805fc853987abe6f7fe3ad97a6a6f3077a16391fec744f671a015fbd7e.Time) { 85 | if m != nil { 86 | m.deletedDateTime = value 87 | } 88 | } 89 | 90 | // Entity 91 | type Entity struct { 92 | // Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. 93 | additionalData map[string]interface{} 94 | // Read-only. 95 | id *string 96 | } 97 | 98 | // NewEntity instantiates a new entity and sets the default values. 99 | func NewEntity() *Entity { 100 | m := &Entity{} 101 | m.SetAdditionalData(make(map[string]interface{})) 102 | return m 103 | } 104 | 105 | // GetAdditionalData gets the additionalData property value. Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. 106 | func (m *Entity) GetAdditionalData() map[string]interface{} { 107 | if m == nil { 108 | return nil 109 | } else { 110 | return m.additionalData 111 | } 112 | } 113 | 114 | // GetId gets the id property value. Read-only. 115 | func (m *Entity) GetId() *string { 116 | if m == nil { 117 | return nil 118 | } else { 119 | return m.id 120 | } 121 | } 122 | 123 | // GetFieldDeserializers the deserialization information for the current model 124 | func (m *Entity) GetFieldDeserializers() map[string]func(i04eb5309aeaafadd28374d79c8471df9b267510b4dc2e3144c378c50f6fd7b55.ParseNode) error { 125 | res := make(map[string]func(i04eb5309aeaafadd28374d79c8471df9b267510b4dc2e3144c378c50f6fd7b55.ParseNode) error) 126 | res["id"] = func(n i04eb5309aeaafadd28374d79c8471df9b267510b4dc2e3144c378c50f6fd7b55.ParseNode) error { 127 | val, err := n.GetStringValue() 128 | if err != nil { 129 | return err 130 | } 131 | if val != nil { 132 | m.SetId(val) 133 | } 134 | return nil 135 | } 136 | return res 137 | } 138 | func (m *Entity) IsNil() bool { 139 | return m == nil 140 | } 141 | 142 | // Serialize serializes information the current object 143 | func (m *Entity) Serialize(writer i04eb5309aeaafadd28374d79c8471df9b267510b4dc2e3144c378c50f6fd7b55.SerializationWriter) error { 144 | { 145 | err := writer.WriteStringValue("id", m.GetId()) 146 | if err != nil { 147 | return err 148 | } 149 | } 150 | { 151 | err := writer.WriteAdditionalData(m.GetAdditionalData()) 152 | if err != nil { 153 | return err 154 | } 155 | } 156 | return nil 157 | } 158 | 159 | // SetAdditionalData sets the additionalData property value. Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. 160 | func (m *Entity) SetAdditionalData(value map[string]interface{}) { 161 | if m != nil { 162 | m.additionalData = value 163 | } 164 | } 165 | 166 | // SetId sets the id property value. Read-only. 167 | func (m *Entity) SetId(value *string) { 168 | if m != nil { 169 | m.id = value 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /internal/user_delta_response.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | i04eb5309aeaafadd28374d79c8471df9b267510b4dc2e3144c378c50f6fd7b55 "github.com/microsoft/kiota-abstractions-go/serialization" 5 | ) 6 | 7 | // UsersDeltaResponse 8 | type UsersDeltaResponse struct { 9 | // 10 | UsersResponse 11 | // 12 | odataDeltaLink *string 13 | } 14 | 15 | // NewDeltasResponse instantiates a new usersResponse and sets the default values. 16 | func NewUsersDeltaResponse() *UsersDeltaResponse { 17 | m := &UsersDeltaResponse{ 18 | UsersResponse: *NewUsersResponse(), 19 | } 20 | return m 21 | } 22 | 23 | // GetOdataDeltaLink gets the @odata.nextLink property value. 24 | func (m *UsersDeltaResponse) GetOdataDeltaLink() *string { 25 | if m == nil { 26 | return nil 27 | } else { 28 | return m.odataDeltaLink 29 | } 30 | } 31 | 32 | // GetFieldDeserializers the deserialization information for the current model 33 | func (m *UsersDeltaResponse) GetFieldDeserializers() map[string]func(i04eb5309aeaafadd28374d79c8471df9b267510b4dc2e3144c378c50f6fd7b55.ParseNode) error { 34 | res := m.UsersResponse.GetFieldDeserializers() 35 | res["@odata.deltaLink"] = func(n i04eb5309aeaafadd28374d79c8471df9b267510b4dc2e3144c378c50f6fd7b55.ParseNode) error { 36 | val, err := n.GetStringValue() 37 | if err != nil { 38 | return err 39 | } 40 | if val != nil { 41 | m.SetOdataDeltaLink(val) 42 | } 43 | return nil 44 | } 45 | return res 46 | } 47 | 48 | // Serialize serializes information the current object 49 | func (m *UsersDeltaResponse) Serialize(writer i04eb5309aeaafadd28374d79c8471df9b267510b4dc2e3144c378c50f6fd7b55.SerializationWriter) error { 50 | { 51 | err := writer.WriteStringValue("@odata.deltaLink", m.GetOdataDeltaLink()) 52 | if err != nil { 53 | return err 54 | } 55 | } 56 | 57 | return m.UsersResponse.Serialize(writer) 58 | } 59 | 60 | // SetOdataDeltaLink sets the @odata.deltaLink property value. 61 | func (m *UsersDeltaResponse) SetOdataDeltaLink(value *string) { 62 | if m != nil { 63 | m.odataDeltaLink = value 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /internal/user_response.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | i04eb5309aeaafadd28374d79c8471df9b267510b4dc2e3144c378c50f6fd7b55 "github.com/microsoft/kiota-abstractions-go/serialization" 5 | ) 6 | 7 | // UsersResponse 8 | type UsersResponse struct { 9 | // Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. 10 | additionalData map[string]interface{} 11 | // 12 | odataNextLink *string 13 | // 14 | value []User 15 | } 16 | 17 | // NewUsersResponse instantiates a new usersResponse and sets the default values. 18 | func NewUsersResponse() *UsersResponse { 19 | m := &UsersResponse{} 20 | m.SetAdditionalData(make(map[string]interface{})) 21 | return m 22 | } 23 | 24 | // GetAdditionalData gets the additionalData property value. Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. 25 | func (m *UsersResponse) GetAdditionalData() map[string]interface{} { 26 | if m == nil { 27 | return nil 28 | } else { 29 | return m.additionalData 30 | } 31 | } 32 | 33 | // GetOdataNextLink gets the @odata.nextLink property value. 34 | func (m *UsersResponse) GetOdataNextLink() *string { 35 | if m == nil { 36 | return nil 37 | } else { 38 | return m.odataNextLink 39 | } 40 | } 41 | 42 | // GetValue gets the value property value. 43 | func (m *UsersResponse) GetValue() []User { 44 | if m == nil { 45 | return nil 46 | } else { 47 | return m.value 48 | } 49 | } 50 | 51 | // GetFieldDeserializers the deserialization information for the current model 52 | func (m *UsersResponse) GetFieldDeserializers() map[string]func(i04eb5309aeaafadd28374d79c8471df9b267510b4dc2e3144c378c50f6fd7b55.ParseNode) error { 53 | res := make(map[string]func(i04eb5309aeaafadd28374d79c8471df9b267510b4dc2e3144c378c50f6fd7b55.ParseNode) error) 54 | res["@odata.nextLink"] = func(n i04eb5309aeaafadd28374d79c8471df9b267510b4dc2e3144c378c50f6fd7b55.ParseNode) error { 55 | val, err := n.GetStringValue() 56 | if err != nil { 57 | return err 58 | } 59 | if val != nil { 60 | m.SetOdataNextLink(val) 61 | } 62 | return nil 63 | } 64 | res["value"] = func(n i04eb5309aeaafadd28374d79c8471df9b267510b4dc2e3144c378c50f6fd7b55.ParseNode) error { 65 | val, err := n.GetCollectionOfObjectValues(func(pn i04eb5309aeaafadd28374d79c8471df9b267510b4dc2e3144c378c50f6fd7b55.ParseNode) (i04eb5309aeaafadd28374d79c8471df9b267510b4dc2e3144c378c50f6fd7b55.Parsable, error) { 66 | return NewUser(), nil 67 | }) 68 | if err != nil { 69 | return err 70 | } 71 | if val != nil { 72 | res := make([]User, len(val)) 73 | for i, v := range val { 74 | res[i] = *(v.(*User)) 75 | } 76 | m.SetValue(res) 77 | } 78 | return nil 79 | } 80 | return res 81 | } 82 | func (m *UsersResponse) IsNil() bool { 83 | return m == nil 84 | } 85 | 86 | // Serialize serializes information the current object 87 | func (m *UsersResponse) Serialize(writer i04eb5309aeaafadd28374d79c8471df9b267510b4dc2e3144c378c50f6fd7b55.SerializationWriter) error { 88 | { 89 | err := writer.WriteStringValue("@odata.nextLink", m.GetOdataNextLink()) 90 | if err != nil { 91 | return err 92 | } 93 | } 94 | { 95 | cast := make([]i04eb5309aeaafadd28374d79c8471df9b267510b4dc2e3144c378c50f6fd7b55.Parsable, len(m.GetValue())) 96 | for i, v := range m.GetValue() { 97 | temp := v 98 | cast[i] = i04eb5309aeaafadd28374d79c8471df9b267510b4dc2e3144c378c50f6fd7b55.Parsable(&temp) 99 | } 100 | err := writer.WriteCollectionOfObjectValues("value", cast) 101 | if err != nil { 102 | return err 103 | } 104 | } 105 | { 106 | err := writer.WriteAdditionalData(m.GetAdditionalData()) 107 | if err != nil { 108 | return err 109 | } 110 | } 111 | return nil 112 | } 113 | 114 | // SetAdditionalData sets the additionalData property value. Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. 115 | func (m *UsersResponse) SetAdditionalData(value map[string]interface{}) { 116 | if m != nil { 117 | m.additionalData = value 118 | } 119 | } 120 | 121 | // SetOdataNextLink sets the @odata.nextLink property value. 122 | func (m *UsersResponse) SetOdataNextLink(value *string) { 123 | if m != nil { 124 | m.odataNextLink = value 125 | } 126 | } 127 | 128 | // SetValue sets the value property value. 129 | func (m *UsersResponse) SetValue(value []User) { 130 | if m != nil { 131 | m.value = value 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /page_iterator.go: -------------------------------------------------------------------------------- 1 | package msgraphgocore 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/url" 7 | "reflect" 8 | 9 | abstractions "github.com/microsoft/kiota-abstractions-go" 10 | "github.com/microsoft/kiota-abstractions-go/serialization" 11 | ) 12 | 13 | const PageIteratorErrorRegistryKey = "PAGE_ITERATOR_ERROR_REGISTRY_KEY" 14 | 15 | // PageIterator represents an iterator object that can be used to get subsequent pages of a collection. 16 | type PageIterator[T interface{}] struct { 17 | currentPage PageResult[T] 18 | reqAdapter abstractions.RequestAdapter 19 | pauseIndex int 20 | constructorFunc serialization.ParsableFactory 21 | headers *abstractions.RequestHeaders 22 | reqOptions []abstractions.RequestOption 23 | errorMappings abstractions.ErrorMappings 24 | } 25 | 26 | // PageResult represents a page object built from a graph response object 27 | type PageResult[T interface{}] struct { 28 | oDataNextLink *string 29 | oDataDeltaLink *string 30 | value []T 31 | } 32 | 33 | func (p *PageResult[T]) getValue() []T { 34 | if p == nil { 35 | return nil 36 | } 37 | 38 | return p.value 39 | } 40 | 41 | func (p *PageResult[T]) getOdataNextLink() *string { 42 | if p == nil { 43 | return nil 44 | } 45 | 46 | return p.oDataNextLink 47 | } 48 | 49 | // NewPageIterator creates an iterator instance 50 | // 51 | // It has three parameters. res is the graph response from the initial request and represents the first page. 52 | // reqAdapter is used for getting the next page and constructorFunc is used for serializing next page's response to the specified type. 53 | func NewPageIterator[T interface{}](res interface{}, reqAdapter abstractions.RequestAdapter, constructorFunc serialization.ParsableFactory) (*PageIterator[T], error) { 54 | if reqAdapter == nil { 55 | return nil, errors.New("reqAdapter can't be nil") 56 | } 57 | 58 | page, err := convertToPage[T](res) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | errorMapping := getErrorMapper(PageIteratorErrorRegistryKey) 64 | 65 | return &PageIterator[T]{ 66 | currentPage: page, 67 | reqAdapter: reqAdapter, 68 | pauseIndex: 0, 69 | constructorFunc: constructorFunc, 70 | headers: abstractions.NewRequestHeaders(), 71 | errorMappings: errorMapping, 72 | }, nil 73 | } 74 | 75 | // Iterate traverses all pages and enumerates all items in the current page and returns an error if something goes wrong. 76 | // 77 | // Iterate receives a callback function which is called with each item in the current page as an argument. The callback function 78 | // returns a boolean. To traverse and enumerate all pages always return true and to pause traversal and enumeration 79 | // return false from the callback. 80 | // 81 | // Example 82 | // 83 | // pageIterator, err := NewPageIterator(resp, reqAdapter, parsableFactory) 84 | // callbackFunc := func (pageItem interface{}) bool { 85 | // fmt.Println(page item.GetDisplayName()) 86 | // return true 87 | // } 88 | // err := pageIterator.Iterate(context.Background(), callbackFunc) 89 | func (pI *PageIterator[T]) Iterate(context context.Context, callback func(pageItem T) bool) error { 90 | for { 91 | keepIterating := pI.enumerate(callback) 92 | 93 | if !keepIterating { 94 | // Callback returned false, stop iterating through pages. 95 | return nil 96 | } 97 | 98 | if pI.currentPage.getOdataNextLink() == nil || *pI.currentPage.getOdataNextLink() == "" { 99 | return nil 100 | } 101 | 102 | nextPage, err := pI.next(context) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | pI.currentPage = nextPage 108 | pI.pauseIndex = 0 // when moving to the next page reset pauseIndex 109 | } 110 | } 111 | 112 | // SetHeaders provides headers for requests made to get subsequent pages 113 | // 114 | // Headers in the initial request -- request to get the first page -- are not included in subsequent page requests. 115 | func (pI *PageIterator[T]) SetHeaders(headers *abstractions.RequestHeaders) { 116 | pI.headers = headers 117 | } 118 | 119 | // SetReqOptions provides configuration for handlers during requests for subsequent pages 120 | func (pI *PageIterator[T]) SetReqOptions(reqOptions []abstractions.RequestOption) { 121 | pI.reqOptions = reqOptions 122 | } 123 | 124 | // GetOdataNextLink returns the @odata.nextLink value in the current page result. 125 | func (pI *PageIterator[T]) GetOdataNextLink() *string { 126 | return pI.currentPage.oDataNextLink 127 | } 128 | 129 | // GetOdataDeltaLink returns the @odata.deltaLink value in current paged result. 130 | func (pI *PageIterator[T]) GetOdataDeltaLink() *string { 131 | return pI.currentPage.oDataDeltaLink 132 | } 133 | 134 | func (pI *PageIterator[T]) next(context context.Context) (PageResult[T], error) { 135 | var page PageResult[T] 136 | 137 | resp, err := pI.fetchNextPage(context) 138 | if err != nil { 139 | return page, err 140 | } 141 | 142 | page, err = convertToPage[T](resp) 143 | if err != nil { 144 | return page, err 145 | } 146 | 147 | return page, nil 148 | } 149 | 150 | func (pI *PageIterator[T]) fetchNextPage(context context.Context) (serialization.Parsable, error) { 151 | var graphResponse serialization.Parsable 152 | var err error 153 | 154 | if pI.currentPage.getOdataNextLink() == nil { 155 | return graphResponse, nil 156 | } 157 | 158 | nextLink, err := url.Parse(*pI.currentPage.getOdataNextLink()) 159 | if err != nil { 160 | return nil, errors.New("parsing nextLink url failed") 161 | } 162 | 163 | requestInfo := abstractions.NewRequestInformation() 164 | requestInfo.Method = abstractions.GET 165 | requestInfo.SetUri(*nextLink) 166 | requestInfo.Headers.AddAll(pI.headers) 167 | requestInfo.AddRequestOptions(pI.reqOptions) 168 | 169 | graphResponse, err = pI.reqAdapter.Send(context, requestInfo, pI.constructorFunc, pI.errorMappings) 170 | if err != nil { 171 | return nil, err 172 | } 173 | 174 | return graphResponse, nil 175 | } 176 | 177 | func (pI *PageIterator[T]) enumerate(callback func(item T) bool) bool { 178 | keepIterating := true 179 | 180 | pageItems := pI.currentPage.getValue() 181 | if pageItems == nil { 182 | return false 183 | } 184 | 185 | // the current page has no items to enumerate 186 | if pI.currentPage.getValue() == nil { 187 | return false 188 | } 189 | 190 | // start/continue enumerating page items from pauseIndex. 191 | // this makes it possible to resume iteration from where we paused iteration. 192 | for i := pI.pauseIndex; i < len(pageItems); i++ { 193 | keepIterating = callback(pageItems[i]) 194 | 195 | // Set pauseIndex so that we know where to resume from. 196 | // Resumes from the next item 197 | pI.pauseIndex = i + 1 198 | 199 | if !keepIterating { 200 | break 201 | } 202 | } 203 | 204 | return keepIterating 205 | } 206 | 207 | // PageWithOdataNextLink represents a contract with the GetOdataNextLink() method 208 | type PageWithOdataNextLink interface { 209 | GetOdataNextLink() *string 210 | } 211 | 212 | // PageWithOdataDeltaLink represents a contract with the GetOdataDeltaLink() method 213 | type PageWithOdataDeltaLink interface { 214 | GetOdataDeltaLink() *string 215 | } 216 | 217 | func convertToPage[T interface{}](response interface{}) (PageResult[T], error) { 218 | var page PageResult[T] 219 | 220 | if response == nil { 221 | return page, errors.New("response cannot be nil") 222 | } 223 | 224 | method := reflect.ValueOf(response).MethodByName("GetValue") 225 | if method.Kind() == reflect.Invalid { 226 | return page, errors.New("value property missing in response object") 227 | } 228 | value := method.Call(nil)[0] 229 | 230 | // Collect all entities in the value slice. 231 | // This converts a graph slice ie []graph.User to a dynamic slice []interface{} 232 | collected := make([]T, 0) 233 | for i := 0; i < value.Len(); i++ { 234 | collected = append(collected, value.Index(i).Interface().(T)) 235 | } 236 | 237 | parsablePage, ok := response.(PageWithOdataNextLink) 238 | if !ok { 239 | return page, errors.New("response does not have next link accessor") 240 | } 241 | 242 | deltablePage, ok := response.(PageWithOdataDeltaLink) 243 | if ok { 244 | page.oDataDeltaLink = deltablePage.GetOdataDeltaLink() 245 | } 246 | 247 | page.oDataNextLink = parsablePage.GetOdataNextLink() 248 | page.value = collected 249 | 250 | return page, nil 251 | } 252 | -------------------------------------------------------------------------------- /page_iterator_test.go: -------------------------------------------------------------------------------- 1 | package msgraphgocore 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/stretchr/testify/require" 8 | nethttp "net/http" 9 | httptest "net/http/httptest" 10 | testing "testing" 11 | 12 | abstractions "github.com/microsoft/kiota-abstractions-go" 13 | "github.com/microsoft/kiota-abstractions-go/authentication" 14 | "github.com/microsoft/kiota-abstractions-go/serialization" 15 | jsonserialization "github.com/microsoft/kiota-serialization-json-go" 16 | "github.com/microsoftgraph/msgraph-sdk-go-core/internal" 17 | "github.com/stretchr/testify/assert" 18 | ) 19 | 20 | func init() { 21 | abstractions.RegisterDefaultSerializer(func() serialization.SerializationWriterFactory { 22 | return jsonserialization.NewJsonSerializationWriterFactory() 23 | }) 24 | abstractions.RegisterDefaultDeserializer(func() serialization.ParseNodeFactory { 25 | return jsonserialization.NewJsonParseNodeFactory() 26 | }) 27 | } 28 | 29 | var reqAdapter, _ = NewGraphRequestAdapterBase(&authentication.AnonymousAuthenticationProvider{}, GraphClientOptions{ 30 | GraphServiceVersion: "", 31 | GraphServiceLibraryVersion: "", 32 | }) 33 | 34 | func ParsableCons(pn serialization.ParseNode) (serialization.Parsable, error) { 35 | return internal.NewUsersResponse(), nil 36 | } 37 | 38 | func ParsableDeltaCons(pn serialization.ParseNode) (serialization.Parsable, error) { 39 | return internal.NewUsersDeltaResponse(), nil 40 | } 41 | 42 | func TestConstructorWithInvalidRequestAdapter(t *testing.T) { 43 | graphResponse := internal.NewUsersResponse() 44 | 45 | _, err := NewPageIterator[internal.User](graphResponse, nil, ParsableCons) 46 | 47 | assert.NotNil(t, err) 48 | } 49 | 50 | func TestConstructorWithInvalidGraphResponse(t *testing.T) { 51 | graphResponse := internal.NewInvalidUsersResponse() 52 | 53 | _, err := NewPageIterator[internal.User](graphResponse, reqAdapter, ParsableCons) 54 | 55 | assert.NotNil(t, err) 56 | } 57 | 58 | func TestConstructorWithInvalidUserGraphResponse(t *testing.T) { 59 | graphResponse := internal.NewInvalidUsersResponse() 60 | 61 | nextLink := "next-page" 62 | users := make([]internal.User, 0) 63 | 64 | graphResponse.SetNextLink(&nextLink) 65 | graphResponse.SetValue(users) 66 | 67 | _, err := NewPageIterator[internal.User](graphResponse, reqAdapter, ParsableCons) 68 | 69 | assert.NotNil(t, err) 70 | } 71 | 72 | func TestPageIteratorHandlesHTTPError(t *testing.T) { 73 | errorMapping := abstractions.ErrorMappings{ 74 | "4XX": internal.CreateSampleErrorFromDiscriminatorValue, 75 | "5XX": internal.CreateSampleErrorFromDiscriminatorValue, 76 | } 77 | // register errorMapper 78 | err := RegisterError(PageIteratorErrorRegistryKey, errorMapping) 79 | require.NoError(t, err) 80 | 81 | testServer := httptest.NewServer(nethttp.HandlerFunc(func(w nethttp.ResponseWriter, req *nethttp.Request) { 82 | w.Header().Set("Content-Type", "application/json") 83 | w.WriteHeader(403) 84 | fmt.Fprint(w, "{}") 85 | })) 86 | defer testServer.Close() 87 | 88 | graphResponse := buildGraphResponse() 89 | mockPath := testServer.URL + "/next-page" 90 | graphResponse.SetOdataNextLink(&mockPath) 91 | 92 | pageIterator, _ := NewPageIterator[internal.User](graphResponse, reqAdapter, ParsableCons) 93 | headers := abstractions.NewRequestHeaders() 94 | headers.Add("ConsistencyLevel", "eventual") 95 | pageIterator.SetHeaders(headers) 96 | res := make([]string, 0) 97 | 98 | err = pageIterator.Iterate(context.Background(), func(item internal.User) bool { 99 | res = append(res, *item.GetDisplayName()) 100 | return true 101 | }) 102 | 103 | var sampleError *internal.SampleError 104 | switch { 105 | case errors.As(err, &sampleError): 106 | assert.Equal(t, "error status code received from the API", err.Error()) 107 | default: 108 | assert.Fail(t, "error type is not as expected") 109 | } 110 | 111 | err = DeRegisterError(PageIteratorErrorRegistryKey) 112 | require.NoError(t, err) 113 | } 114 | 115 | func TestIterateStopsWhenCallbackReturnsFalse(t *testing.T) { 116 | res := make([]string, 0) 117 | graphResponse := buildGraphResponse() 118 | testServer := httptest.NewServer(nethttp.HandlerFunc(func(w nethttp.ResponseWriter, req *nethttp.Request) { 119 | w.Header().Set("Content-Type", "application/json") 120 | fmt.Fprint(w, ` 121 | { 122 | "@odata.nextLink": "", 123 | "value": [ 124 | { 125 | "id": "10" 126 | } 127 | ] 128 | } 129 | `) 130 | assert.NotNil(t, req.Header["ConsistencyLevel"]) 131 | })) 132 | defer testServer.Close() 133 | pageIterator, _ := NewPageIterator[internal.User](graphResponse, reqAdapter, ParsableCons) 134 | headers := abstractions.NewRequestHeaders() 135 | headers.Add("ConsistencyLevel", "eventual") 136 | pageIterator.SetHeaders(headers) 137 | 138 | err := pageIterator.Iterate(context.Background(), func(item internal.User) bool { 139 | res = append(res, *item.GetDisplayName()) 140 | return !(*item.GetId() == "2") 141 | }) 142 | if err != nil { 143 | t.Error(err) 144 | } 145 | 146 | assert.Equal(t, len(res), 3) 147 | } 148 | 149 | func TestIterateEnumeratesAllPages(t *testing.T) { 150 | testServer := httptest.NewServer(nethttp.HandlerFunc(func(w nethttp.ResponseWriter, req *nethttp.Request) { 151 | w.Header().Set("Content-Type", "application/json") 152 | fmt.Fprint(w, ` 153 | { 154 | "@odata.nextLink": "", 155 | "value": [ 156 | { 157 | "id": "10" 158 | } 159 | ] 160 | } 161 | `) 162 | 163 | })) 164 | defer testServer.Close() 165 | 166 | graphResponse := buildGraphResponse() 167 | mockPath := testServer.URL + "/next-page" 168 | graphResponse.SetOdataNextLink(&mockPath) 169 | 170 | pageIterator, _ := NewPageIterator[internal.User](graphResponse, reqAdapter, ParsableCons) 171 | res := make([]string, 0) 172 | 173 | err := pageIterator.Iterate(context.Background(), func(item internal.User) bool { 174 | res = append(res, *item.GetId()) 175 | return true 176 | }) 177 | 178 | // Initial page has 5 items and the next page has 1 item. 179 | assert.Equal(t, len(res), 6) 180 | assert.Nil(t, err) 181 | } 182 | 183 | func TestIterateCanBePausedAndResumed(t *testing.T) { 184 | res := make([]string, 0) 185 | res2 := make([]string, 0) 186 | 187 | testServer := httptest.NewServer(nethttp.HandlerFunc(func(w nethttp.ResponseWriter, req *nethttp.Request) { 188 | w.Header().Set("Content-Type", "application/json") 189 | fmt.Fprint(w, ` 190 | { 191 | "@odata.nextLink": "", 192 | "value": [ 193 | { 194 | "id": "10" 195 | } 196 | ] 197 | } 198 | `) 199 | 200 | })) 201 | defer testServer.Close() 202 | 203 | response := buildGraphResponse() 204 | mockPath := testServer.URL + "/next-page" 205 | response.SetOdataNextLink(&mockPath) 206 | 207 | pageIterator, _ := NewPageIterator[internal.User](response, reqAdapter, ParsableCons) 208 | pageIterator.Iterate(context.Background(), func(item internal.User) bool { 209 | res = append(res, *item.GetId()) 210 | 211 | return *item.GetId() != "4" 212 | }) 213 | 214 | assert.Equal(t, res, []string{"0", "1", "2", "3", "4"}) 215 | assert.Equal(t, pageIterator.GetOdataNextLink(), response.GetOdataNextLink()) 216 | 217 | pageIterator.Iterate(context.Background(), func(item internal.User) bool { 218 | res2 = append(res2, *item.GetId()) 219 | 220 | return true 221 | }) 222 | assert.Equal(t, res2, []string{"10"}) 223 | assert.Empty(t, pageIterator.GetOdataNextLink()) 224 | 225 | pageIterator.Iterate(context.Background(), func(item internal.User) bool { 226 | assert.Fail(t, "Should not re-iterate over items") 227 | return true 228 | }) 229 | } 230 | 231 | func TestGetOdataNextLink(t *testing.T) { 232 | testServer := httptest.NewServer(nethttp.HandlerFunc(func(w nethttp.ResponseWriter, req *nethttp.Request) { 233 | w.Header().Set("Content-Type", "application/json") 234 | fmt.Fprint(w, ` 235 | { 236 | "@odata.nextLink": "", 237 | "value": [ 238 | { 239 | "id": "10" 240 | } 241 | ] 242 | } 243 | `) 244 | })) 245 | defer testServer.Close() 246 | 247 | graphResponse := buildGraphResponse() 248 | mockPath := testServer.URL + "/next-page" 249 | graphResponse.SetOdataNextLink(&mockPath) 250 | 251 | pageIterator, _ := NewPageIterator[internal.User](graphResponse, reqAdapter, ParsableDeltaCons) 252 | pageIterator.Iterate(context.Background(), func(item internal.User) bool { 253 | return true 254 | }) 255 | 256 | assert.Empty(t, pageIterator.GetOdataNextLink()) 257 | } 258 | 259 | func TestGetOdataDeltaLink(t *testing.T) { 260 | testServer := httptest.NewServer(nethttp.HandlerFunc(func(w nethttp.ResponseWriter, req *nethttp.Request) { 261 | w.Header().Set("Content-Type", "application/json") 262 | fmt.Fprint(w, ` 263 | { 264 | "@odata.nextLink": "", 265 | "@odata.deltaLink": "delta-page-2", 266 | "value": [ 267 | { 268 | "id": "10" 269 | } 270 | ] 271 | } 272 | `) 273 | })) 274 | defer testServer.Close() 275 | 276 | dl := "delta-page-1" 277 | mockPath := testServer.URL + "/next-page" 278 | 279 | graphResponse := &internal.UsersDeltaResponse{ 280 | UsersResponse: *buildGraphResponse(), 281 | } 282 | graphResponse.SetOdataDeltaLink(&dl) 283 | graphResponse.SetOdataNextLink(&mockPath) 284 | 285 | pageIterator, _ := NewPageIterator[internal.User](graphResponse, reqAdapter, ParsableDeltaCons) 286 | pageIterator.Iterate(context.Background(), func(item internal.User) bool { 287 | return true 288 | }) 289 | 290 | assert.Equal(t, *pageIterator.GetOdataDeltaLink(), "delta-page-2") 291 | } 292 | 293 | func buildGraphResponse() *internal.UsersResponse { 294 | var res = internal.NewUsersResponse() 295 | 296 | nextLink := "next-page" 297 | users := make([]internal.User, 0) 298 | 299 | for i := 0; i < 5; i++ { 300 | u := internal.NewUser() 301 | id := fmt.Sprint(i) 302 | u.SetId(&id) 303 | 304 | users = append(users, *u) 305 | } 306 | 307 | res.SetOdataNextLink(&nextLink) 308 | res.SetValue(users) 309 | 310 | return res 311 | } 312 | 313 | func Test_convertToPage(t *testing.T) { 314 | type args struct { 315 | response interface{} 316 | } 317 | tests := []struct { 318 | name string 319 | args args 320 | want PageResult[validTestStruct] 321 | wantErr bool 322 | }{ 323 | { 324 | name: "should pass", 325 | args: args{ 326 | response: &validTestStruct{ 327 | obj: []validTestStruct{ 328 | { 329 | obj: []validTestStruct{ 330 | { 331 | obj: []validTestStruct{}, 332 | }, 333 | }, 334 | }, 335 | }}, 336 | }, 337 | want: PageResult[validTestStruct]{ 338 | oDataNextLink: nil, 339 | oDataDeltaLink: nil, 340 | value: []validTestStruct{ 341 | { 342 | obj: []validTestStruct{ 343 | { 344 | obj: []validTestStruct{}, 345 | }, 346 | }, 347 | }, 348 | }, 349 | }, 350 | }, 351 | { 352 | name: "should return error 'saying response cannot be nil' for nil response", 353 | args: args{ 354 | response: nil, 355 | }, 356 | want: PageResult[validTestStruct]{ 357 | oDataNextLink: nil, 358 | oDataDeltaLink: nil, 359 | value: nil, 360 | }, 361 | wantErr: true, 362 | }, 363 | { 364 | name: "should return error 'value property missing in response object' for missing 'GetValue' method", 365 | args: args{ 366 | response: &invalidTestStruct{ 367 | obj: []invalidTestStruct{ 368 | { 369 | obj: []invalidTestStruct{ 370 | { 371 | obj: []invalidTestStruct{}, 372 | }, 373 | }, 374 | }, 375 | }}, 376 | }, 377 | want: PageResult[validTestStruct]{ 378 | oDataNextLink: nil, 379 | oDataDeltaLink: nil, 380 | }, 381 | wantErr: true, 382 | }, 383 | { 384 | name: "should return error 'response does not have next link accessor' for missing 'GetOdataNextLink() *string' method", 385 | args: args{ 386 | response: &invalidTestStruct{ 387 | obj: []invalidTestStruct{ 388 | { 389 | obj: []invalidTestStruct{ 390 | { 391 | obj: []invalidTestStruct{}, 392 | }, 393 | }, 394 | }, 395 | }}, 396 | }, 397 | want: PageResult[validTestStruct]{ 398 | oDataNextLink: nil, 399 | oDataDeltaLink: nil, 400 | }, 401 | wantErr: true, 402 | }, 403 | } 404 | for _, tt := range tests { 405 | t.Run(tt.name, func(t *testing.T) { 406 | got, err := convertToPage[validTestStruct](tt.args.response) 407 | if (err != nil) != tt.wantErr { 408 | t.Errorf("convertToPage() error = %v, wantErr %v", err, tt.wantErr) 409 | return 410 | } 411 | assert.Equal(t, tt.want.oDataDeltaLink, got.oDataDeltaLink, "got %v, want %v", got.oDataNextLink, tt.want.oDataNextLink) 412 | assert.Equal(t, tt.want.oDataNextLink, got.oDataNextLink, "got %v, want %v", got.oDataDeltaLink, tt.want.oDataDeltaLink) 413 | assert.Equal(t, tt.want.value, got.value, "got %v, want %v", got.value, tt.want.value) 414 | }) 415 | } 416 | } 417 | 418 | type validTestStruct struct { 419 | obj []validTestStruct 420 | } 421 | 422 | func (t *validTestStruct) GetValue() []validTestStruct { 423 | return t.obj 424 | } 425 | 426 | func (t *validTestStruct) GetOdataNextLink() *string { 427 | return nil 428 | } 429 | 430 | func (t *validTestStruct) GetOdataDeltaLink() *string { 431 | return nil 432 | } 433 | 434 | type invalidTestStruct struct { 435 | obj []invalidTestStruct 436 | } 437 | 438 | func (t *invalidTestStruct) GetOdataDeltaLink() *string { 439 | return nil 440 | } 441 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "release-type": "go", 3 | "bump-minor-pre-major": true, 4 | "bump-patch-for-minor-pre-major": true, 5 | "include-component-in-tag": false, 6 | "include-v-in-tag": true, 7 | "packages": { 8 | ".": { 9 | "package-name": "github.com/microsoftgraph/msgraph-sdk-go-core", 10 | "changelog-path": "CHANGELOG.md", 11 | "extra-files": [ 12 | "version.go" 13 | ] 14 | } 15 | }, 16 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" 17 | } 18 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=microsoftgraph_msgraph-sdk-go-core 2 | sonar.organization=microsoftgraph2 3 | sonar.exclusions=**/*_test.go 4 | sonar.test.inclusions=**/*_test.go 5 | sonar.go.tests.reportPaths=result.out 6 | sonar.go.coverage.reportPaths=cover.out -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package msgraphgocore 2 | 3 | /** The SDK version */ 4 | // x-release-please-start-version 5 | var CoreVersion = "1.3.2" 6 | 7 | // x-release-please-end 8 | --------------------------------------------------------------------------------