diff --git a/.credo.exs b/.credo.exs new file mode 100644 index 0000000..8c0278c --- /dev/null +++ b/.credo.exs @@ -0,0 +1,223 @@ +# This file contains the configuration for Credo and you are probably reading +# this after creating it with `mix credo.gen.config`. +# +# If you find anything wrong or unclear in this file, please report an +# issue on GitHub: https://github.com/rrrene/credo/issues +# +%{ + # + # You can have as many configs as you like in the `configs:` field. + configs: [ + %{ + # + # Run any config using `mix credo -C `. If no config name is given + # "default" is used. + # + name: "default", + # + # These are the files included in the analysis: + files: %{ + # + # You can give explicit globs or simply directories. + # In the latter case `**/*.{ex,exs}` will be used. + # + included: [ + "examples/", + "lib/", + "src/", + "test/" + ], + excluded: [~r"/lib/mix", ~r"/_build/", ~r"/deps/", ~r"/node_modules/"] + }, + # + # Load and configure plugins here: + # + plugins: [], + # + # If you create your own checks, you must specify the source files for + # them here, so they can be loaded by Credo before running the analysis. + # + requires: [], + # + # If you want to enforce a style guide and need a more traditional linting + # experience, you can change `strict` to `true` below: + # + strict: true, + # + # To modify the timeout for parsing files, change this value: + # + parse_timeout: 5000, + # + # If you want to use uncolored output by default, you can change `color` + # to `false` below: + # + color: true, + # + # You can customize the parameters of any check by adding a second element + # to the tuple. + # + # To disable a check put `false` as second element: + # + # {Credo.Check.Design.DuplicatedCode, false} + # + checks: %{ + enabled: [ + # + ## Consistency Checks + # + {Credo.Check.Consistency.ExceptionNames, []}, + {Credo.Check.Consistency.LineEndings, []}, + {Credo.Check.Consistency.ParameterPatternMatching, []}, + {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, + {Credo.Check.Consistency.SpaceAroundOperators, []}, + {Credo.Check.Consistency.SpaceInParentheses, []}, + {Credo.Check.Consistency.TabsOrSpaces, []}, + + # + ## Design Checks + # + # You can customize the priority of any check + # Priority values are: `low, normal, high, higher` + # + {Credo.Check.Design.AliasUsage, + [priority: :normal, if_nested_deeper_than: 1, if_called_more_often_than: 0]}, + {Credo.Check.Design.TagFIXME, []}, + # You can also customize the exit_status of each check. + # If you don't want TODO comments to cause `mix credo` to fail, just + # set this value to 0 (zero). + # + {Credo.Check.Design.TagTODO, [exit_status: 2]}, + + # + ## Readability Checks + # + {Credo.Check.Readability.AliasOrder, []}, + {Credo.Check.Readability.FunctionNames, []}, + {Credo.Check.Readability.LargeNumbers, []}, + {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, + {Credo.Check.Readability.ModuleAttributeNames, []}, + {Credo.Check.Readability.ModuleDoc, []}, + {Credo.Check.Readability.ModuleNames, []}, + {Credo.Check.Readability.ParenthesesInCondition, []}, + # {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, + {Credo.Check.Readability.PipeIntoAnonymousFunctions, []}, + {Credo.Check.Readability.PredicateFunctionNames, []}, + {Credo.Check.Readability.PreferImplicitTry, []}, + {Credo.Check.Readability.RedundantBlankLines, []}, + {Credo.Check.Readability.Semicolons, []}, + {Credo.Check.Readability.SpaceAfterCommas, []}, + {Credo.Check.Readability.StringSigils, []}, + {Credo.Check.Readability.TrailingBlankLine, []}, + {Credo.Check.Readability.TrailingWhiteSpace, []}, + {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, + {Credo.Check.Readability.VariableNames, []}, + {Credo.Check.Readability.WithSingleClause, []}, + + # + ## Refactoring Opportunities + # + {Credo.Check.Refactor.Apply, []}, + {Credo.Check.Refactor.CondStatements, []}, + {Credo.Check.Refactor.CyclomaticComplexity, []}, + {Credo.Check.Refactor.FilterCount, []}, + {Credo.Check.Refactor.FilterFilter, []}, + {Credo.Check.Refactor.FunctionArity, []}, + {Credo.Check.Refactor.LongQuoteBlocks, []}, + {Credo.Check.Refactor.MapJoin, []}, + {Credo.Check.Refactor.MatchInCondition, []}, + {Credo.Check.Refactor.NegatedConditionsInUnless, []}, + {Credo.Check.Refactor.NegatedConditionsWithElse, []}, + {Credo.Check.Refactor.Nesting, []}, + {Credo.Check.Refactor.RedundantWithClauseResult, []}, + {Credo.Check.Refactor.RejectReject, []}, + {Credo.Check.Refactor.UnlessWithElse, []}, + {Credo.Check.Refactor.WithClauses, []}, + + # + ## Warnings + # + {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, + {Credo.Check.Warning.BoolOperationOnSameValues, []}, + {Credo.Check.Warning.Dbg, []}, + {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, + {Credo.Check.Warning.IExPry, []}, + {Credo.Check.Warning.IoInspect, []}, + {Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []}, + {Credo.Check.Warning.OperationOnSameValues, []}, + {Credo.Check.Warning.OperationWithConstantResult, []}, + {Credo.Check.Warning.RaiseInsideRescue, []}, + {Credo.Check.Warning.SpecWithStruct, []}, + {Credo.Check.Warning.UnsafeExec, []}, + {Credo.Check.Warning.UnusedEnumOperation, []}, + {Credo.Check.Warning.UnusedFileOperation, []}, + {Credo.Check.Warning.UnusedKeywordOperation, []}, + {Credo.Check.Warning.UnusedListOperation, []}, + {Credo.Check.Warning.UnusedPathOperation, []}, + {Credo.Check.Warning.UnusedRegexOperation, []}, + {Credo.Check.Warning.UnusedStringOperation, []}, + {Credo.Check.Warning.UnusedTupleOperation, []}, + {Credo.Check.Warning.WrongTestFileExtension, []}, + + # Enabled controversial and experimental checks + {Credo.Check.Consistency.UnusedVariableNames, []}, + {Credo.Check.Design.DuplicatedCode, []}, + {Credo.Check.Readability.ImplTrue, []}, + {Credo.Check.Readability.Specs, []}, + {Credo.Check.Readability.StrictModuleLayout, []}, + {Credo.Check.Refactor.IoPuts, []}, + {Credo.Check.Warning.MixEnv, []} + ], + disabled: [ + # + # Checks scheduled for next check update (opt-in for now) + {Credo.Check.Refactor.UtcNowTruncate, []}, + + # + # Controversial and experimental checks (opt-in, just move the check to `:enabled` + # and be sure to use `mix credo --strict` to see low priority checks) + # + {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, + # {Credo.Check.Consistency.UnusedVariableNames, []}, + # {Credo.Check.Design.DuplicatedCode, []}, + {Credo.Check.Design.SkipTestWithoutComment, []}, + {Credo.Check.Readability.AliasAs, []}, + {Credo.Check.Readability.BlockPipe, []}, + # {Credo.Check.Readability.ImplTrue, []}, + {Credo.Check.Readability.MultiAlias, []}, + {Credo.Check.Readability.NestedFunctionCalls, []}, + {Credo.Check.Readability.OneArityFunctionInPipe, []}, + {Credo.Check.Readability.OnePipePerLine, []}, + {Credo.Check.Readability.SeparateAliasRequire, []}, + {Credo.Check.Readability.SingleFunctionToBlockPipe, []}, + {Credo.Check.Readability.SinglePipe, []}, + # {Credo.Check.Readability.Specs, []}, + # {Credo.Check.Readability.StrictModuleLayout, []}, + {Credo.Check.Readability.WithCustomTaggedTuple, []}, + {Credo.Check.Refactor.ABCSize, []}, + {Credo.Check.Refactor.AppendSingleItem, []}, + {Credo.Check.Refactor.DoubleBooleanNegation, []}, + {Credo.Check.Refactor.FilterReject, []}, + # {Credo.Check.Refactor.IoPuts, []}, + {Credo.Check.Refactor.MapMap, []}, + {Credo.Check.Refactor.ModuleDependencies, []}, + {Credo.Check.Refactor.NegatedIsNil, []}, + {Credo.Check.Refactor.PassAsyncInTestCases, []}, + {Credo.Check.Refactor.PipeChainStart, []}, + {Credo.Check.Refactor.RejectFilter, []}, + {Credo.Check.Refactor.VariableRebinding, []}, + {Credo.Check.Warning.LazyLogging, []}, + {Credo.Check.Warning.LeakyEnvironment, []}, + {Credo.Check.Warning.MapGetUnsafePass, []}, + # {Credo.Check.Warning.MixEnv, []}, + {Credo.Check.Warning.UnsafeToAtom, []} + + # {Credo.Check.Refactor.MapInto, []}, + + # + # Custom checks can be created using `mix credo.gen.check`. + # + ] + } + } + ] +} diff --git a/.iex.exs b/.iex.exs new file mode 100644 index 0000000..eb5be44 --- /dev/null +++ b/.iex.exs @@ -0,0 +1,7 @@ +creds = %{ + client_id: System.fetch_env!("MICROSOFT_CLIENT_ID"), + client_secret: System.fetch_env!("MICROSOFT_CLIENT_SECRET"), + tenant_id: System.fetch_env!("MICROSOFT_TENANT_ID") +} + +client = Msg.Client.new(creds) diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..3c8bc19 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,37 @@ +{ + "default": true, + "gitignore": true, + "ignores": [ + "node_modules/" + ], + "line-length": false, + "no-duplicate-heading": false, + "no-inline-html": { + "allowed_elements": ["br", "kbd", "sub", "sup"] + }, + "whitespace": true, + "no-hard-tabs": true, + "no-trailing-spaces": true, + "no-multiple-blanks": { + "maximum": 2 + }, + "blanks-around-headings": { + "lines_above": 1, + "lines_below": 1 + }, + "heading-increment": true, + "no-missing-space-atx": true, + "no-missing-space-closed-atx": true, + "ul-style": { + "style": "dash" + }, + "ol-prefix": { + "style": "one_or_ordered" + }, + "list-indent": true, + "blanks-around-fences": true, + "fenced-code-language": true, + "first-line-h1": false, + "heading-start-left": true, + "single-trailing-newline": true +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..592e741 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,84 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is `msg`, an Elixir library for accessing Microsoft 365 data using the Microsoft Graph API. It's designed specifically for applications using OAuth2 client credentials flow (application-only authentication). + +## Development Commands + +```bash +# Install dependencies +mix deps.get + +# Run tests +mix test + +# Run tests with coverage +mix coveralls + +# Format code +mix format + +# Generate documentation (docs environment) +mix docs + +# Run comprehensive quality checks +mix quality + +# Skip slow Dialyzer step in quality check +mix quality --skip-dialyzer + +# Run static analysis only +mix credo --strict + +# Run type checking +mix dialyzer + +# Run a single test file +mix test test/path/to/test_file.exs + +# Run integration tests +mix test test/msg/integration/ +``` + +## Quality Standards + +This project enforces strict code quality via the `mix quality` task (`lib/mix/tasks/quality.ex:1`), which runs: + +- Code formatting (auto-fixed) +- Trailing whitespace removal (auto-fixed) +- Markdown linting (auto-fixed if markdownlint-cli2 available) +- Test coverage check (requires >90%) +- Static analysis with Credo in strict mode +- Type checking with Dialyzer + +## Architecture + +The library follows a layered architecture: + +- **Msg.Client** (`lib/msg/client.ex:1`): Handles OAuth2 authentication and creates configured Req clients with Bearer tokens for Graph API requests +- **Msg.Request** (`lib/msg/request.ex:1`): Provides low-level HTTP request helpers with response handling and error parsing +- **Msg.Users** (`lib/msg/users.ex:1`): Higher-level API wrapper for `/users` endpoints - pattern for other Graph API modules + +### Key Dependencies + +- **Req**: HTTP client for making API requests +- **OAuth2**: Client credentials flow implementation +- **Jason**: JSON encoding/decoding +- **Mox**: Test mocking framework +- **Credo**: Static code analysis +- **Dialyzer**: Type checking +- **ExCoveralls**: Test coverage reporting + +### Testing Strategy + +The project uses ExUnit with Mox for mocking. Integration tests are separated in `test/msg/integration/` directory for real API testing scenarios. Coverage must be >90%. + +### Authentication Flow + +The library implements OAuth2 client credentials flow: +1. `Msg.Client.new/1` accepts credentials (`client_id`, `client_secret`, `tenant_id`) +2. `fetch_token!/1` exchanges credentials for access token via Azure AD +3. Returns configured Req client with Authorization header for Graph API calls \ No newline at end of file diff --git a/README.md b/README.md index 2ded169..26f570b 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,8 @@ client = Msg.Client.new(creds) ## Features -* Built on top of Req for HTTP requests -* OAuth2 client credentials flow via oauth2 +- Built on top of Req for HTTP requests +- OAuth2 client credentials flow via oauth2 ## License diff --git a/coveralls.json b/coveralls.json new file mode 100644 index 0000000..03d37b4 --- /dev/null +++ b/coveralls.json @@ -0,0 +1,12 @@ +{ + "skip_files": [ "test/support", "lib/mix/tasks" ], + "coverage_options": { + "treat_no_relevant_lines_as_covered": true, + "output_dir": "cover/", + "minimum_coverage": 90, + "html_filter_full_covered": true + }, + "terminal_options": { + "file_column_width": 50 + } +} diff --git a/lib/mix/tasks/quality.ex b/lib/mix/tasks/quality.ex new file mode 100644 index 0000000..18c6616 --- /dev/null +++ b/lib/mix/tasks/quality.ex @@ -0,0 +1,323 @@ +defmodule Mix.Tasks.Quality do + @shortdoc "Runs complete code quality validation pipeline" + + @moduledoc """ + Runs the complete code quality validation pipeline. + + This task runs the same checks as the pre-push git hook: + - Code formatting check (and auto-fix if needed) + - Trailing whitespace check (and auto-fix if needed) + - Markdown linting (and auto-fix if needed, if markdownlint-cli2 is available) + - Examples tests (validates example applications work correctly) + - Test coverage check (requires >90% coverage) + - Static code analysis with Credo (strict mode) + - Type checking with Dialyzer + + ## Usage + + mix quality + + ## Options + + - `--skip-dialyzer` - Skip the Dialyzer type checking step (faster) + - `--skip-markdown` - Skip markdown linting checks + + ## Examples + + # Run full quality pipeline + mix quality + + # Skip slow Dialyzer step + mix quality --skip-dialyzer + """ + + use Mix.Task + + @switches [ + skip_dialyzer: :boolean, + skip_markdown: :boolean + ] + + @spec run([String.t()]) :: :ok + def run(args) do + {opts, _remaining_args} = OptionParser.parse!(args, switches: @switches) + + Mix.shell().info("๐Ÿ” Running code quality validation pipeline...") + + # Step 1: Code formatting check + check_formatting() + + # Step 2: Trailing whitespace check + check_trailing_whitespace() + + # Step 3: Markdown linting (if available and not skipped) + unless opts[:skip_markdown] do + check_markdown() + end + + # Step 4: Coverage check + run_coverage_check() + + # Step 5: Static analysis + run_static_analysis() + + # Step 6: Type checking (unless skipped) + unless opts[:skip_dialyzer] do + run_type_checking() + end + + Mix.shell().info("โœ… All quality checks passed!") + end + + defp check_formatting do + Mix.Task.run("format", []) + + # Check if files were actually changed by formatting + case System.cmd("git", ["diff", "--quiet"], stderr_to_stdout: true) do + {_output, 0} -> + Mix.shell().info("โœ… No files needed formatting changes.") + + {_output, _exit_code} -> + Mix.shell().info("๐Ÿ“ Code has been automatically formatted.") + + # Mix.shell().info( + # "๐Ÿ”„ Please commit the formatting changes and run quality check again:" + # ) + + # Mix.shell().info(" git add .") + # Mix.shell().info(" git commit -m 'Auto-format code with mix format'") + # Mix.raise("Code was auto-formatted - please commit changes and re-run") + end + end + + defp check_trailing_whitespace do + Mix.shell().info("๐Ÿงน Checking for trailing whitespace...") + + files = find_elixir_files() + files_with_whitespace = filter_files_with_trailing_whitespace(files) + handle_trailing_whitespace_results(files_with_whitespace) + end + + defp find_elixir_files do + case System.cmd( + "find", + ~w[ . -path ./_build -prune -o -path ./deps -prune -o -name *.ex -print -o -name *.exs -print ], + stderr_to_stdout: true + ) do + {output, 0} -> + output + |> String.trim() + |> String.split("\n") + |> Enum.reject(&(&1 == "")) + + {error, _exit_code} -> + Mix.shell().error("โŒ Failed to search for files: #{error}") + Mix.raise("File search failed") + end + end + + defp filter_files_with_trailing_whitespace(files) do + Enum.filter(files, fn file -> + case System.cmd("grep", ["-l", "[[:space:]]$", file], stderr_to_stdout: true) do + {_output, 0} -> true + {_output, _exit_code} -> false + end + end) + end + + defp handle_trailing_whitespace_results([]) do + Mix.shell().info("โœ… No trailing whitespace found") + end + + defp handle_trailing_whitespace_results(files_to_fix) do + Mix.shell().info("โŒ Found trailing whitespace in #{length(files_to_fix)} file(s)") + + Enum.each(files_to_fix, fn file -> + Mix.shell().info(" Cleaning: #{file}") + end) + + clean_trailing_whitespace(files_to_fix) + check_and_handle_git_changes("trailing whitespace", "Remove trailing whitespace") + end + + defp clean_trailing_whitespace(files) do + Enum.each(files, fn file -> + case System.cmd("sed", ["-i", "", "s/[[:space:]]*$//", file], stderr_to_stdout: true) do + {_output, 0} -> :ok + {error, _exit_code} -> Mix.shell().error("Failed to clean #{file}: #{error}") + end + end) + end + + defp check_markdown do + case System.find_executable("markdownlint-cli2") do + nil -> handle_missing_markdownlint() + _path -> check_markdown_files() + end + end + + defp handle_missing_markdownlint do + md_files = find_markdown_files() + + if md_files != [] do + Mix.shell().info("โ„น๏ธ markdownlint-cli2 not found - skipping markdown linting") + Mix.shell().info("๐Ÿ’ก Install with: npm install -g markdownlint-cli2") + end + end + + defp check_markdown_files do + md_files = find_markdown_files() + + if md_files != [] do + Mix.shell().info("๐Ÿ“‹ Checking markdown formatting...") + run_markdown_linting(md_files) + end + end + + defp find_markdown_files do + case System.cmd( + "find", + [ + ".", + "-name", + "*.md", + "-not", + "-path", + "*deps/*", + "-not", + "-path", + "*_build/*", + "-not", + "-path", + "*node_modules/*" + ], + stderr_to_stdout: true + ) do + {output, 0} when output != "" -> + output + |> String.trim() + |> String.split("\n") + |> Enum.reject(&(&1 == "")) + + _no_files_or_error -> + [] + end + end + + defp run_markdown_linting(md_files) do + case System.cmd( + "markdownlint-cli2", + ["--config", ".markdownlint.json", "#node_modules"] ++ md_files, + stderr_to_stdout: true + ) do + {_output, 0} -> + Mix.shell().info("โœ… Markdown formatting looks good") + + {_output, _exit_code} -> + Mix.shell().info("โŒ Markdown linting issues found. Running auto-fix...") + attempt_markdown_autofix(md_files) + end + end + + defp attempt_markdown_autofix(md_files) do + case System.cmd( + "markdownlint-cli2", + ["--config", ".markdownlint.json", "--fix"] ++ md_files, + stderr_to_stdout: true + ) do + {_fix_output, 0} -> + Mix.shell().info("โœ… Markdown issues were automatically fixed") + check_and_handle_git_changes("markdown", "Fix markdown formatting") + + {fix_error, _fix_exit_code} -> + handle_markdown_autofix_failure(fix_error, md_files) + end + end + + @spec handle_markdown_autofix_failure(String.t(), [String.t()]) :: no_return() + defp handle_markdown_autofix_failure(fix_error, md_files) do + Mix.shell().error("โŒ Automatic markdown fixing failed!") + Mix.shell().info("๐Ÿ’ก Manual fix may be required. Error output:") + Mix.shell().info(fix_error) + Mix.shell().info("๐Ÿ’ก Try running manually:") + + Mix.shell().info( + ~s[ markdownlint-cli2 --config .markdownlint.json --fix] <> + " " <> Enum.join(md_files, " ") + ) + + Mix.raise("Markdown linting failed") + end + + defp check_and_handle_git_changes(change_type, _commit_message) do + case System.cmd("git", ["diff", "--quiet"], stderr_to_stdout: true) do + {_output, 0} -> + Mix.shell().info("โœ… No files were actually modified") + + {_output, _exit_code} -> + Mix.shell().info("๐Ÿ“ #{String.capitalize(change_type)} has been automatically fixed.") + + # Mix.shell().info("๐Ÿ”„ Please commit the #{change_type} fixes and run quality check again:") + # Mix.shell().info(" git add .") + # Mix.shell().info(" git commit -m '#{commit_message}'") + # + # Mix.raise( + # "#{String.capitalize(change_type)} was auto-fixed - please commit changes and re-run" + # ) + end + end + + defp run_coverage_check do + Mix.shell().info("๐Ÿ“Š Running test coverage check...") + + case System.cmd("env", ["MIX_ENV=test", "mix", "coveralls"], stderr_to_stdout: true) do + {_output, 0} -> + Mix.shell().info("โœ… Coverage check passed (>90% required)") + + {output, _exit_code} -> + # Extract the coverage percentage from the output + coverage_line = + output + |> String.split("\n") + |> Enum.find(&String.contains?(&1, "[TOTAL]")) + + case coverage_line do + nil -> + Mix.shell().error("โŒ Coverage check failed - could not determine coverage percentage") + + line -> + Mix.shell().error("โŒ Coverage check failed: #{String.trim(line)}") + end + + Mix.shell().info("๐Ÿ’ก Run 'MIX_ENV=test mix coveralls.detail' to see uncovered lines") + Mix.shell().info("๐Ÿ’ก Add more tests to increase coverage above 90%") + Mix.raise("Coverage check failed") + end + end + + defp run_static_analysis do + Mix.shell().info("๐Ÿ” Running static analysis (Credo)...") + + try do + Mix.Task.run("credo", ["--strict"]) + Mix.shell().info("โœ… Static analysis passed") + rescue + e in Mix.Error -> + Mix.shell().error("โŒ Credo analysis failed. Fix issues before proceeding.") + Mix.raise("Static analysis failed: #{Exception.message(e)}") + end + end + + defp run_type_checking do + Mix.shell().info("๐Ÿ”ฌ Running type checking (Dialyzer)...") + + try do + Mix.Task.run("dialyzer", []) + Mix.shell().info("โœ… Type checking passed") + rescue + e in Mix.Error -> + Mix.shell().error("โŒ Dialyzer type checking failed.") + Mix.raise("Type checking failed: #{Exception.message(e)}") + end + end +end diff --git a/lib/msg/users.ex b/lib/msg/users.ex index 0dfe256..af8c0a4 100644 --- a/lib/msg/users.ex +++ b/lib/msg/users.ex @@ -19,5 +19,11 @@ defmodule Msg.Users do @spec list(Req.Request.t()) :: {:ok, map()} | {:error, any()} def list(client) do Request.get(client, "/users") + |> case do + {:ok, %{"value" => value}} -> + {:ok, value} + error -> + error + end end end diff --git a/mix.exs b/mix.exs index dbf8e32..5a6ef7f 100644 --- a/mix.exs +++ b/mix.exs @@ -2,6 +2,22 @@ defmodule Msg.MixProject do use Mix.Project @version "0.1.1" + @deps [ + # Docs - separated out to speed up dev compilcation + {:ex_doc, "~> 0.31", only: :docs, runtime: false}, + + # Development, Test, Local + {:castore, "~> 1.0", only: [:dev, :test]}, + {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, + {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, + {:excoveralls, "~> 0.18", only: :test}, + {:mox, "~> 1.1", only: :test}, + + # Actual dependencies + {:jason, "~> 1.4"}, + {:oauth2, "~> 2.0"}, + {:req, "~> 0.4"} + ] def project do [ @@ -9,11 +25,17 @@ defmodule Msg.MixProject do version: @version, elixir: "~> 1.16", start_permanent: Mix.env() == :prod, - deps: deps(), + deps: @deps, description: "Microsoft Graph for Elixir", package: [ licenses: ["MIT"], links: %{"GitHub" => "https://github.com/riddler/msg"} + ], + test_coverage: [tool: ExCoveralls], + dialyzer: [ + plt_file: {:no_warn, "priv/plts/dialyzer.plt"}, + plt_add_apps: [:mix, :ex_unit], + warnings: [:unknown] ] ] end @@ -24,16 +46,15 @@ defmodule Msg.MixProject do ] end - defp deps do + def cli do [ - # Testing and development - {:ex_doc, "~> 0.31", only: :dev, runtime: false}, - {:mox, "~> 1.1", only: :test}, - - # Actual dependencies - {:jason, "~> 1.4"}, - {:oauth2, "~> 2.0"}, - {:req, "~> 0.4"} + preferred_envs: [ + coveralls: :test, + "coveralls.detail": :test, + "coveralls.json": :test, + docs: :docs, + quality: :test + ] ] end end diff --git a/mix.lock b/mix.lock index e5774a6..7a4a93e 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,13 @@ %{ + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, + "castore": {:hex, :castore, "1.0.15", "8aa930c890fe18b6fe0a0cff27b27d0d4d231867897bd23ea772dee561f032a3", [:mix], [], "hexpm", "96ce4c69d7d5d7a0761420ef743e2f4096253931a3ba69e5ff8ef1844fe446d3"}, + "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, + "dialyxir": {:hex, :dialyxir, "1.4.6", "7cca478334bf8307e968664343cbdb432ee95b4b68a9cba95bdabb0ad5bdfd9a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "8cf5615c5cd4c2da6c501faae642839c8405b49f8aa057ad4ae401cb808ef64d"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, + "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, "ex_doc": {:hex, :ex_doc, "0.38.1", "bae0a0bd5b5925b1caef4987e3470902d072d03347114ffe03a55dbe206dd4c2", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "754636236d191b895e1e4de2ebb504c057fe1995fdfdd92e9d75c4b05633008b"}, + "excoveralls": {:hex, :excoveralls, "0.18.5", "e229d0a65982613332ec30f07940038fe451a2e5b29bce2a5022165f0c9b157e", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "523fe8a15603f86d64852aab2abe8ddbd78e68579c8525ae765facc5eae01562"}, + "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},