From 94ae17e7692f4d322bad630d0807a10d91c73a13 Mon Sep 17 00:00:00 2001 From: {503} Date: Thu, 12 Mar 2026 23:00:26 -0500 Subject: [PATCH 01/22] rubocop -A auto-corrections --- .github/workflows/ci.yml | 25 +++++++++ .github/workflows/rubocop-analysis.yml | 50 ----------------- .github/workflows/sourcehawk-scan.yml | 20 ------- .rubocop.yml | 52 +++++++++++++----- CHANGELOG.md | 2 +- CLAUDE.md | 64 ++++++++++++++++++++++ CODE_OF_CONDUCT.md | 75 -------------------------- CONTRIBUTING.md | 55 ------------------- Gemfile | 2 + INDIVIDUAL_CONTRIBUTOR_LICENSE.md | 30 ----------- LICENSE | 2 +- NOTICE.txt | 9 ---- README.md | 51 ++++++++---------- SECURITY.md | 9 ---- attribution.txt | 1 - legion-json.gemspec | 22 ++++---- lib/legion/json.rb | 2 + lib/legion/json/invalid_json.rb | 2 + lib/legion/json/parse_error.rb | 2 + lib/legion/json/version.rb | 4 +- sourcehawk.yml | 4 -- spec/legion/json_spec.rb | 2 + spec/spec_helper.rb | 2 + 23 files changed, 179 insertions(+), 308 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/rubocop-analysis.yml delete mode 100644 .github/workflows/sourcehawk-scan.yml create mode 100644 CLAUDE.md delete mode 100644 CODE_OF_CONDUCT.md delete mode 100644 CONTRIBUTING.md delete mode 100644 INDIVIDUAL_CONTRIBUTOR_LICENSE.md delete mode 100644 NOTICE.txt delete mode 100644 SECURITY.md delete mode 100644 attribution.txt delete mode 100644 sourcehawk.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4f213db --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,25 @@ +name: CI +on: [push, pull_request] + +jobs: + rubocop: + name: RuboCop + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.4' + bundler-cache: true + - run: bundle exec rubocop + + rspec: + name: RSpec + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.4' + bundler-cache: true + - run: bundle exec rspec diff --git a/.github/workflows/rubocop-analysis.yml b/.github/workflows/rubocop-analysis.yml deleted file mode 100644 index b0813f6..0000000 --- a/.github/workflows/rubocop-analysis.yml +++ /dev/null @@ -1,50 +0,0 @@ -# pulled from repo -name: "Rubocop" - -on: - push: - branches: [ main ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ main ] - schedule: - - cron: '45 7 * * 2' - -jobs: - rubocop: - runs-on: ubuntu-latest - strategy: - fail-fast: false - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - # If running on a self-hosted runner, check it meets the requirements - # listed at https://github.com/ruby/setup-ruby#using-self-hosted-runners - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: 2.6 - - # This step is not necessary if you add the gem to your Gemfile - - name: Install Code Scanning integration - run: bundle add code-scanning-rubocop --version 0.3.0 --skip-install - - - name: Install dependencies - run: bundle install - - - name: Install Rubocop - run: gem install rubocop - - - name: Rubocop run - run: | - bash -c " - bundle exec rubocop --require code_scanning --format CodeScanning::SarifFormatter -o rubocop.sarif - [[ $? -ne 2 ]] - " - - - name: Upload Sarif output - uses: github/codeql-action/upload-sarif@v1 - with: - sarif_file: rubocop.sarif diff --git a/.github/workflows/sourcehawk-scan.yml b/.github/workflows/sourcehawk-scan.yml deleted file mode 100644 index 72a2af8..0000000 --- a/.github/workflows/sourcehawk-scan.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Sourcehawk Scan -on: - push: - branches: - - main - - master - pull_request: - branches: - - main - - master -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Sourcehawk Scan - uses: optum/sourcehawk-scan-github-action@main - - - diff --git a/.rubocop.yml b/.rubocop.yml index 5739cd8..785cccf 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,22 +1,48 @@ +AllCops: + TargetRubyVersion: 3.4 + NewCops: enable + SuggestExtensions: false + Layout/LineLength: - Max: 120 - Exclude: - - '*.gemspec' + Max: 160 + +Layout/SpaceAroundEqualsInParameterDefault: + EnforcedStyle: space + +Layout/HashAlignment: + EnforcedHashRocketStyle: table + EnforcedColonStyle: table + Metrics/MethodLength: - Max: 30 + Max: 50 + Metrics/ClassLength: Max: 1500 + +Metrics/ModuleLength: + Max: 1500 + Metrics/BlockLength: - Max: 50 -Style/OptionalBooleanParameter: - Enabled: false + Max: 40 + +Metrics/AbcSize: + Max: 60 + +Metrics/CyclomaticComplexity: + Max: 15 + +Metrics/PerceivedComplexity: + Max: 17 + Style/Documentation: Enabled: false -AllCops: - TargetRubyVersion: 2.5 - NewCops: enable - SuggestExtensions: false + +Style/SymbolArray: + Enabled: true + Style/FrozenStringLiteralComment: + Enabled: true + EnforcedStyle: always + +Naming/FileName: Enabled: false -Gemspec/RequiredRubyVersion: - Enabled: false \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c9d351..9b6c55c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ # Legion::JSON ## v1.2.0 -Moving from BitBucket to GitHub inside the Optum org. All git history is reset from this point on \ No newline at end of file +Moving from BitBucket to GitHub. All git history is reset from this point on \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..809117c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,64 @@ +# legion-json: JSON Wrapper for LegionIO + +**Repository Level 3 Documentation** +- **Category**: `/Users/miverso2/rubymine/arc/CLAUDE.md` +- **Workspace**: `/Users/miverso2/rubymine/CLAUDE.md` + +## Purpose + +JSON wrapper module for the LegionIO framework. Wraps `multi_json` and `json_pure` to provide a consistent JSON interface across all Legion gems and extensions. Automatically uses faster C-extension JSON gems (like `oj`) when available. + +**GitHub**: https://github.com/Optum/legion-json +**License**: Apache-2.0 + +## Architecture + +``` +Legion::Json +├── .load(string, symbolize_keys: true) # Deserialize JSON -> Hash +├── .dump(hash) # Serialize Hash -> JSON +├── InvalidJson # Custom error class +└── ParseError # JSON parse error class +``` + +### Key Design Patterns + +- **Thin Wrapper**: Delegates to `MultiJson.load` / `MultiJson.dump` +- **Symbolized Keys by Default**: `symbolize_keys: true` is the default (unlike standard JSON) +- **Auto C-Extension**: If `oj` gem is installed, `multi_json` automatically uses it for performance + +## Dependencies + +| Gem | Purpose | +|-----|---------| +| `multi_json` | JSON adapter abstraction | +| `json_pure` | Pure-Ruby JSON fallback | + +## File Map + +| Path | Purpose | +|------|---------| +| `lib/legion/json.rb` | Module entry with `load`/`dump` methods | +| `lib/legion/json/invalid_json.rb` | InvalidJson error class | +| `lib/legion/json/parse_error.rb` | ParseError class | +| `lib/legion/json/version.rb` | VERSION constant | + +## Role in LegionIO + +**Foundation gem** - used by nearly every other Legion gem. `legion-settings` depends on it directly for config file parsing. All message serialization flows through this module. + +``` +legion-json + ^ + |-- legion-settings + | ^ + | |-- legion-cache + | |-- legion-data + | |-- legion-transport + | └-- LegionIO + └-- legion-transport (direct dependency) +``` + +--- + +**Maintained By**: Matthew Iverson (@Esity) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index 52c7f95..0000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,75 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, gender identity and expression, level of experience, -nationality, personal appearance, race, religion, or sexual identity and -orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or -advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project email -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at [opensource@optum.com][email]. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at [http://contributor-covenant.org/version/1/4][version] - -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ -[email]: mailto:opensource@optum.com \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index b0c397d..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,55 +0,0 @@ -# Contribution Guidelines - -Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. Please also review our [Contributor License Agreement ("CLA")](INDIVIDUAL_CONTRIBUTOR_LICENSE.md) prior to submitting changes to the project. You will need to attest to this agreement following the instructions in the [Paperwork for Pull Requests](#paperwork-for-pull-requests) section below. - ---- - -# How to Contribute - -Now that we have the disclaimer out of the way, let's get into how you can be a part of our project. There are many different ways to contribute. - -## Issues - -We track our work using Issues in GitHub. Feel free to open up your own issue to point out areas for improvement or to suggest your own new experiment. If you are comfortable with signing the waiver linked above and contributing code or documentation, grab your own issue and start working. - -## Coding Standards - -We have some general guidelines towards contributing to this project. -Please run RSpec and Rubocop while developing code for LegionIO - -### Languages - -*Ruby* - -## Pull Requests - -If you've gotten as far as reading this section, then thank you for your suggestions. - -## Paperwork for Pull Requests - -* Please read this guide and make sure you agree with our [Contributor License Agreement ("CLA")](INDIVIDUAL_CONTRIBUTOR_LICENSE.md). -* Make sure git knows your name and email address: - ``` - $ git config user.name "J. Random User" - $ git config user.email "j.random.user@example.com" - ``` ->The name and email address must be valid as we cannot accept anonymous contributions. -* Write good commit messages. -> Concise commit messages that describe your changes help us better understand your contributions. -* The first time you open a pull request in this repository, you will see a comment on your PR with a link that will allow you to sign our Contributor License Agreement (CLA) if necessary. -> The link will take you to a page that allows you to view our CLA. You will need to click the `Sign in with GitHub to agree button` and authorize the cla-assistant application to access the email addresses associated with your GitHub account. Agreeing to the CLA is also considered to be an attestation that you either wrote or have the rights to contribute the code. All committers to the PR branch will be required to sign the CLA, but you will only need to sign once. This CLA applies to all repositories in the Optum org. - -## General Guidelines - -Ensure your pull request (PR) adheres to the following guidelines: - -* Try to make the name concise and descriptive. -* Give a good description of the change being made. Since this is very subjective, see the [Updating Your Pull Request (PR)](#updating-your-pull-request-pr) section below for further details. -* Every pull request should be associated with one or more issues. If no issue exists yet, please create your own. -* Make sure that all applicable issues are mentioned somewhere in the PR description. This can be done by typing # to bring up a list of issues. - -### Updating Your Pull Request (PR) - -A lot of times, making a PR adhere to the standards above can be difficult. If the maintainers notice anything that we'd like changed, we'll ask you to edit your PR before we merge it. This applies to both the content documented in the PR and the changed contained within the branch being merged. There's no need to open a new PR. Just edit the existing one. - -[email]: mailto:opensource@optum.com \ No newline at end of file diff --git a/Gemfile b/Gemfile index edaf657..f6c3759 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source 'https://rubygems.org' gemspec diff --git a/INDIVIDUAL_CONTRIBUTOR_LICENSE.md b/INDIVIDUAL_CONTRIBUTOR_LICENSE.md deleted file mode 100644 index 79460dc..0000000 --- a/INDIVIDUAL_CONTRIBUTOR_LICENSE.md +++ /dev/null @@ -1,30 +0,0 @@ -# Individual Contributor License Agreement ("Agreement") V2.0 - -Thank you for your interest in this Optum project (the "PROJECT"). In order to clarify the intellectual property license granted with Contributions from any person or entity, the PROJECT must have a Contributor License Agreement ("CLA") on file that has been signed by each Contributor, indicating agreement to the license terms below. This license is for your protection as a Contributor as well as the protection of the PROJECT and its users; it does not change your rights to use your own Contributions for any other purpose. - -You accept and agree to the following terms and conditions for Your present and future Contributions submitted to the PROJECT. In return, the PROJECT shall not use Your Contributions in a way that is inconsistent with stated project goals in effect at the time of the Contribution. Except for the license granted herein to the PROJECT and recipients of software distributed by the PROJECT, You reserve all right, title, and interest in and to Your Contributions. -1. Definitions. - -"You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with the PROJECT. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to the PROJECT for inclusion in, or documentation of, any of the products owned or managed by the PROJECT (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the PROJECT or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the PROJECT for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." - -2. Grant of Copyright License. - -Subject to the terms and conditions of this Agreement, You hereby grant to the PROJECT and to recipients of software distributed by the PROJECT a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works. - -3. Grant of Patent License. - -Subject to the terms and conditions of this Agreement, You hereby grant to the PROJECT and to recipients of software distributed by the PROJECT a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed. - -4. Representations. - - (a) You represent that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that your employer has waived such rights for your Contributions to the PROJECT, or that your employer has executed a separate Corporate CLA with the PROJECT. - - (b) You represent that each of Your Contributions is Your original creation (see section 6 for submissions on behalf of others). You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions. - -5. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. - -6. Should You wish to submit work that is not Your original creation, You may submit it to the PROJECT separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]". - -7. You agree to notify the PROJECT of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect. \ No newline at end of file diff --git a/LICENSE b/LICENSE index 93234d8..20cba51 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2021 Optum + Copyright 2021 Esity Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/NOTICE.txt b/NOTICE.txt deleted file mode 100644 index af1c864..0000000 --- a/NOTICE.txt +++ /dev/null @@ -1,9 +0,0 @@ -Legion::Json(legion-json) -Copyright 2021 Optum - -Project Description: -==================== -A JSON wrapper module for the LegionIO framework - -Author(s): -Esity \ No newline at end of file diff --git a/README.md b/README.md index 5a6cc4c..800291e 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,38 @@ -Legion::Json -===== +# legion-json -Legion::Json is the JSON wrapper that is used by the LegionIO framework. It gives all other gems and extensions a single -json library to use for consistency. It wraps [multi_json](https://rubygems.org/gems/multi_json) and also [json_pure](https://rubygems.org/gems/json_pure) +JSON wrapper module for the [LegionIO](https://github.com/LegionIO/LegionIO) framework. Wraps `multi_json` and `json_pure` to provide a consistent JSON interface across all Legion gems and extensions. Automatically uses faster C-extension JSON gems (like `oj`) when available. -Additional C extension style json gems can be installed like [oj](https://rubygems.org/gems/oj) and it they should be -used automatically. +## Installation -Supported Ruby versions and implementations ------------------------------------------------- +```bash +gem install legion-json +``` -Legion::Json should work identically on: +Or add to your Gemfile: - * JRuby 9.2+ - * Ruby 2.4+ +```ruby +gem 'legion-json' +``` +## Usage -Installation and Usage ------------------------- +```ruby +require 'legion/json' -You can verify your installation using this piece of code: +json_string = '{"foo":"bar","nested":{"hello":"world"}}' +Legion::Json.load(json_string) # => {foo: "bar", nested: {hello: "world"}} +Legion::Json.load(json_string, symbolize_keys: false) # => {"foo" => "bar", ...} -```bash -gem install legion-json +hash = { foo: 'bar', nested: { hello: 'world' } } +Legion::Json.dump(hash) # => '{"foo":"bar","nested":{"hello":"world"}}' ``` -```ruby -require 'legion-json' -json_string = '{"foo":"bar","nested":{"hello":"world"}}' -Legion::Json.load(json_string) -Legion::Json.load(json_string, symbolize_keys: false) # symbolize_keys defaults to true +Keys are symbolized by default, unlike standard Ruby JSON. -hash = {foo: 'bar', nested: {hello: 'world'}} -Legion::Json.dump(hash) -Legion::Json.dump(hash, ) +## Requirements -``` +- Ruby >= 3.4 -Authors ----------- +## License -* [Matthew Iverson](https://github.com/Esity) - current maintainer +Apache-2.0 diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index acc4d53..0000000 --- a/SECURITY.md +++ /dev/null @@ -1,9 +0,0 @@ -# Security Policy - -## Supported Versions -| Version | Supported | -| ------- | ------------------ | -| 1.x.x | :white_check_mark: | - -## Reporting a Vulnerability -To be added diff --git a/attribution.txt b/attribution.txt deleted file mode 100644 index e4c875c..0000000 --- a/attribution.txt +++ /dev/null @@ -1 +0,0 @@ -Add attributions here. \ No newline at end of file diff --git a/legion-json.gemspec b/legion-json.gemspec index 4981d41..418071d 100644 --- a/legion-json.gemspec +++ b/legion-json.gemspec @@ -6,24 +6,24 @@ Gem::Specification.new do |spec| spec.name = 'legion-json' spec.version = Legion::Json::VERSION spec.authors = ['Esity'] - spec.email = %w[matthewdiverson@gmail.com ruby@optum.com] + spec.email = ['matthewdiverson@gmail.com'] spec.summary = 'Wraps multi_json, json_pure and others to allow for interchanging json gems without deploying all new extensions' spec.description = 'A JSON wrapper module for the LegionIO framework' - spec.homepage = 'https://github.com/Optum/legion-json' + spec.homepage = 'https://github.com/LegionIO/legion-json' spec.license = 'Apache-2.0' spec.require_paths = ['lib'] - spec.required_ruby_version = '>= 2.4' + spec.required_ruby_version = '>= 3.4' spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } - spec.test_files = spec.files.select { |p| p =~ %r{^test/.*_test.rb} } - spec.extra_rdoc_files = %w[README.md LICENSE CHANGELOG.md] + spec.extra_rdoc_files = %w[README.md LICENSE CHANGELOG.md] spec.metadata = { - 'bug_tracker_uri' => 'https://github.com/Optum/legion-json/issues', - 'changelog_uri' => 'https://github.com/Optum/legion-json/src/main/CHANGELOG.md', - 'documentation_uri' => 'https://github.com/Optum/legion-json', - 'homepage_uri' => 'https://github.com/Optum/LegionIO', - 'source_code_uri' => 'https://github.com/Optum/legion-json', - 'wiki_uri' => 'https://github.com/Optum/legion-json/wiki' + 'bug_tracker_uri' => 'https://github.com/LegionIO/legion-json/issues', + 'changelog_uri' => 'https://github.com/LegionIO/legion-json/blob/main/CHANGELOG.md', + 'documentation_uri' => 'https://github.com/LegionIO/legion-json', + 'homepage_uri' => 'https://github.com/LegionIO/LegionIO', + 'source_code_uri' => 'https://github.com/LegionIO/legion-json', + 'wiki_uri' => 'https://github.com/LegionIO/legion-json/wiki', + 'rubygems_mfa_required' => 'true' } spec.add_dependency 'json_pure' diff --git a/lib/legion/json.rb b/lib/legion/json.rb index c90d597..cfdf79f 100644 --- a/lib/legion/json.rb +++ b/lib/legion/json.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'legion/json/version' require 'legion/json/parse_error' require 'legion/json/invalid_json' diff --git a/lib/legion/json/invalid_json.rb b/lib/legion/json/invalid_json.rb index c685e8e..1d2ada9 100644 --- a/lib/legion/json/invalid_json.rb +++ b/lib/legion/json/invalid_json.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Legion module Exception class InvalidJson < StandardError; end diff --git a/lib/legion/json/parse_error.rb b/lib/legion/json/parse_error.rb index ee7606a..92af89f 100644 --- a/lib/legion/json/parse_error.rb +++ b/lib/legion/json/parse_error.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Legion module JSON # The Legion JSON parser error abstraction. diff --git a/lib/legion/json/version.rb b/lib/legion/json/version.rb index e26febd..4c01d83 100644 --- a/lib/legion/json/version.rb +++ b/lib/legion/json/version.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + module Legion module Json - VERSION = '1.2.0'.freeze + VERSION = '1.2.0' end end diff --git a/sourcehawk.yml b/sourcehawk.yml deleted file mode 100644 index a228e9b..0000000 --- a/sourcehawk.yml +++ /dev/null @@ -1,4 +0,0 @@ - -config-locations: - - https://raw.githubusercontent.com/optum/.github/main/sourcehawk.yml - diff --git a/spec/legion/json_spec.rb b/spec/legion/json_spec.rb index 7a4b0c2..56f943d 100644 --- a/spec/legion/json_spec.rb +++ b/spec/legion/json_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'legion/json' diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 60a63ca..0ddd9b0 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + begin require 'simplecov' From 3c555be1ae086e78cef9aa9087bd0b57f3fbc2b3 Mon Sep 17 00:00:00 2001 From: {503} Date: Thu, 12 Mar 2026 23:38:24 -0500 Subject: [PATCH 02/22] update rspec for end-to-end coverage of legion-json expand from 7 tests to 45 covering all public methods, options, types, error paths, and edge cases --- spec/legion/json_spec.rb | 283 ++++++++++++++++++++++++++++++++++----- 1 file changed, 247 insertions(+), 36 deletions(-) diff --git a/spec/legion/json_spec.rb b/spec/legion/json_spec.rb index 56f943d..be9bfa0 100644 --- a/spec/legion/json_spec.rb +++ b/spec/legion/json_spec.rb @@ -5,54 +5,265 @@ SimpleCov.command_name 'lib/legion/json' -RSpec.describe Legion::Json do - before(:all) { @json_string = '{"foo":"bar"}' } +RSpec.describe Legion::JSON do + describe '.parser' do + it 'returns MultiJson as the default parser' do + expect(described_class.parser).to eq(MultiJson) + end - it 'has a version number' do - expect(Legion::Json::VERSION).not_to be nil + it 'memoizes the parser' do + expect(described_class.parser).to be(described_class.parser) + end + end + + describe '.load' do + context 'with symbolized keys (default)' do + it 'parses a simple JSON object' do + result = described_class.load('{"foo":"bar"}') + expect(result).to eq(foo: 'bar') + end + + it 'parses nested JSON objects' do + json = '{"outer":{"inner":"value","deep":{"key":"found"}}}' + result = described_class.load(json) + expect(result[:outer][:inner]).to eq('value') + expect(result[:outer][:deep][:key]).to eq('found') + end + + it 'parses JSON with multiple keys' do + json = '{"a":"1","b":"2","c":"3"}' + result = described_class.load(json) + expect(result).to eq(a: '1', b: '2', c: '3') + end + end + + context 'with symbolize_keys: false' do + it 'returns string keys' do + result = described_class.load('{"foo":"bar"}', symbolize_keys: false) + expect(result).to eq('foo' => 'bar') + expect(result.keys.first).to be_a(String) + end + + it 'returns string keys for nested objects' do + json = '{"outer":{"inner":"value"}}' + result = described_class.load(json, symbolize_keys: false) + expect(result['outer']['inner']).to eq('value') + end + end + + context 'with various JSON types' do + it 'parses integers' do + result = described_class.load('{"count":42}') + expect(result[:count]).to eq(42) + end + + it 'parses floats' do + result = described_class.load('{"price":19.99}') + expect(result[:price]).to eq(19.99) + end + + it 'parses booleans' do + result = described_class.load('{"active":true,"deleted":false}') + expect(result[:active]).to be true + expect(result[:deleted]).to be false + end + + it 'parses null values' do + result = described_class.load('{"value":null}') + expect(result[:value]).to be_nil + expect(result).to have_key(:value) + end + + it 'parses arrays' do + result = described_class.load('{"items":[1,2,3]}') + expect(result[:items]).to eq([1, 2, 3]) + end + + it 'parses top-level arrays' do + result = described_class.load('[1,"two",3]') + expect(result).to eq([1, 'two', 3]) + end + + it 'parses an empty object' do + result = described_class.load('{}') + expect(result).to eq({}) + end + + it 'parses an empty array' do + result = described_class.load('[]') + expect(result).to eq([]) + end + + it 'parses unicode strings' do + result = described_class.load('{"greeting":"héllo wörld 🌍"}') + expect(result[:greeting]).to eq('héllo wörld 🌍') + end + + it 'parses strings with escaped characters' do + result = described_class.load('{"text":"line1\\nline2\\ttab"}') + expect(result[:text]).to eq("line1\nline2\ttab") + end + end + + context 'round-trip fidelity' do + it 'preserves data through load -> dump -> load' do + original = '{"name":"test","count":5,"active":true,"tags":["a","b"]}' + hash = described_class.load(original) + json = described_class.dump(hash) + reloaded = described_class.load(json) + expect(reloaded).to eq(hash) + end + end + end + + describe '.dump' do + it 'serializes a simple hash' do + result = described_class.dump({ foo: 'bar' }) + expect(result).to eq('{"foo":"bar"}') + end + + it 'serializes nested hashes' do + result = described_class.dump({ a: { b: 'c' } }) + expect(described_class.load(result)).to eq(a: { b: 'c' }) + end + + it 'serializes arrays' do + result = described_class.dump([1, 2, 3]) + expect(result).to eq('[1,2,3]') + end + + it 'serializes nil values' do + result = described_class.dump({ key: nil }) + expect(result).to eq('{"key":null}') + end + + it 'serializes booleans' do + result = described_class.dump({ yes: true, no: false }) + parsed = described_class.load(result) + expect(parsed[:yes]).to be true + expect(parsed[:no]).to be false + end + + it 'serializes an empty hash' do + expect(described_class.dump({})).to eq('{}') + end + + it 'serializes an empty array' do + expect(described_class.dump([])).to eq('[]') + end + + context 'with pretty: true' do + it 'produces formatted output with newlines' do + result = described_class.dump({ foo: 'bar' }, pretty: true) + expect(result).to include("\n") + expect(described_class.load(result)).to eq(foo: 'bar') + end + + it 'produces valid JSON that round-trips' do + original = { name: 'test', items: [1, 2] } + pretty = described_class.dump(original, pretty: true) + expect(described_class.load(pretty)).to eq(original) + end + end + + context 'with pretty: false (default)' do + it 'produces compact output without extra whitespace' do + result = described_class.dump({ foo: 'bar' }) + expect(result).not_to include("\n") + end + end + end +end + +RSpec.describe Legion::JSON::ParseError do + describe '.build' do + let(:original_error) do + begin + MultiJson.load('{bad json}') + rescue StandardError => e + e + end + end + let(:bad_input) { '{bad json}' } + let(:parse_error) { described_class.build(original_error, bad_input) } + + it 'is a StandardError' do + expect(parse_error).to be_a(StandardError) + end + + it 'preserves the original error message' do + expect(parse_error.message).to eq(original_error.message) + end + + it 'stores the original input as data' do + expect(parse_error.data).to eq(bad_input) + end + + it 'stores the original error as cause' do + expect(parse_error.cause).to eq(original_error) + end + + it 'preserves the original backtrace' do + expect(parse_error.backtrace).to eq(original_error.backtrace) + end end - it 'can load a parser for current platform' do - hash = Legion::JSON.load(@json_string) - expect(hash).to eq(foo: 'bar') - string = Legion::JSON.dump({ baz: 'qux' }) - expect(string).to eq('{"baz":"qux"}') + context 'when raised from Legion::JSON.load' do + it 'raises ParseError for unclosed string' do + expect { Legion::JSON.load('{"foo":"bar}') }.to raise_error(described_class) + end + + it 'raises ParseError for unclosed brace' do + expect { Legion::JSON.load('{') }.to raise_error(described_class) + end + + it 'raises ParseError for unquoted keys' do + expect { Legion::JSON.load('{foo:"bar"}') }.to raise_error(described_class) + end + + it 'raises ParseError for trailing comma' do + expect { Legion::JSON.load('{"foo":"bar",}') }.to raise_error(described_class) + end + + it 'raises ParseError for completely invalid input' do + expect { Legion::JSON.load('not json at all') }.to raise_error(described_class) + end + + it 'returns nil for empty string' do + expect(Legion::JSON.load('')).to be_nil + end + + it 'includes the original input in the error' do + bad_input = '{"broken' + begin + Legion::JSON.load(bad_input) + rescue described_class => e + expect(e.data).to eq(bad_input) + end + end end +end - it 'can load json correctly' do - hash = Legion::JSON.load(@json_string) - string = Legion::JSON.dump(hash) - expect(string).to eq(@json_string) +RSpec.describe Legion::Exception::InvalidJson do + it 'is a StandardError' do + expect(described_class.new).to be_a(StandardError) end - it 'can handle large json objects' do - hash = Legion::JSON.load('{"foo":"bar", "cake": "sugar", "test":{"ruby":"gem", "java": "jar"}}') - expect(hash[:foo]).to eq('bar') - expect(hash[:cake]).to eq('sugar') - expect(hash[:test]).not_to be_empty - expect(hash[:test][:ruby]).to eq('gem') - expect(hash[:test][:java]).to eq('jar') - expect(hash[:test][:thing]).to be_nil - expect(hash[:nil]).to be_nil + it 'can be raised and rescued' do + expect { raise described_class, 'bad data' }.to raise_error(described_class, 'bad data') end +end - it 'matches when switched back to json' do - json = '{"foo":"bar"}' - hash = Legion::JSON.load(json) - return_json = Legion::JSON.dump(hash) - expect(return_json).to eq(json) +RSpec.describe Legion::Json do + it 'has a version number' do + expect(Legion::Json::VERSION).not_to be_nil end - it 'will not sq' do - hash = Legion::JSON.load(@json_string) - expect(hash).not_to eq(foo: 'baz') - expect(hash).not_to eq(bar: 'foo') + it 'has a version that is a string' do + expect(Legion::Json::VERSION).to be_a(String) end - it 'will throw exception' do - expect { Legion::JSON.load('{"foo":"bar}') }.to raise_error(Legion::JSON::ParseError) - expect { Legion::JSON.load('{"foo":"bar"') }.to raise_error(Legion::JSON::ParseError) - expect { Legion::JSON.load('{foo:"bar}') }.to raise_error(Legion::JSON::ParseError) - expect { Legion::JSON.load('{') }.to raise_error(Legion::JSON::ParseError) + it 'has a version matching semver format' do + expect(Legion::Json::VERSION).to match(/\A\d+\.\d+\.\d+\z/) end end From 42e0f45e93e8efe2209614a288e5f88baf1cdd4f Mon Sep 17 00:00:00 2001 From: Esity Date: Thu, 12 Mar 2026 23:50:43 -0500 Subject: [PATCH 03/22] rubocop -A auto-corrections and config updates exclude spec/ from Metrics/BlockLength, fix spec formatting --- .rubocop.yml | 3 +++ spec/legion/json_spec.rb | 8 +++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 785cccf..18c9f92 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -24,6 +24,9 @@ Metrics/ModuleLength: Metrics/BlockLength: Max: 40 + AllowedPatterns: ['describe', 'context', 'it', 'shared_examples'] + Exclude: + - 'spec/**/*' Metrics/AbcSize: Max: 60 diff --git a/spec/legion/json_spec.rb b/spec/legion/json_spec.rb index be9bfa0..4a1d0ab 100644 --- a/spec/legion/json_spec.rb +++ b/spec/legion/json_spec.rb @@ -178,11 +178,9 @@ RSpec.describe Legion::JSON::ParseError do describe '.build' do let(:original_error) do - begin - MultiJson.load('{bad json}') - rescue StandardError => e - e - end + MultiJson.load('{bad json}') + rescue StandardError => e + e end let(:bad_input) { '{bad json}' } let(:parse_error) { described_class.build(original_error, bad_input) } From 3236f6ddbd47ce5cf474c67810757e0ef50bcbfb Mon Sep 17 00:00:00 2001 From: Esity Date: Fri, 13 Mar 2026 01:04:01 -0500 Subject: [PATCH 04/22] reindex documentation to reflect current codebase --- CLAUDE.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 809117c..b2d5a04 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,14 +1,13 @@ # legion-json: JSON Wrapper for LegionIO **Repository Level 3 Documentation** -- **Category**: `/Users/miverso2/rubymine/arc/CLAUDE.md` -- **Workspace**: `/Users/miverso2/rubymine/CLAUDE.md` +- **Parent**: `/Users/miverso2/rubymine/legion/CLAUDE.md` ## Purpose JSON wrapper module for the LegionIO framework. Wraps `multi_json` and `json_pure` to provide a consistent JSON interface across all Legion gems and extensions. Automatically uses faster C-extension JSON gems (like `oj`) when available. -**GitHub**: https://github.com/Optum/legion-json +**GitHub**: https://github.com/LegionIO/legion-json **License**: Apache-2.0 ## Architecture From 4c7cdbec49fcce20f196946e4c2486e7c56b6eb7 Mon Sep 17 00:00:00 2001 From: Esity Date: Fri, 13 Mar 2026 10:45:22 -0500 Subject: [PATCH 05/22] resolve rubocop offenses with auto-correct --- .ruby-version | 1 + 1 file changed, 1 insertion(+) create mode 100644 .ruby-version diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..7921bd0 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.4.8 From 2ad231bb775ec0136633d32d2603619a3f873fc9 Mon Sep 17 00:00:00 2001 From: Esity Date: Fri, 13 Mar 2026 13:01:24 -0500 Subject: [PATCH 06/22] switch to org-level reusable ci workflow --- .github/workflows/ci.yml | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f213db..a298d6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,25 +1,5 @@ name: CI on: [push, pull_request] - jobs: - rubocop: - name: RuboCop - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.4' - bundler-cache: true - - run: bundle exec rubocop - - rspec: - name: RSpec - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.4' - bundler-cache: true - - run: bundle exec rspec + ci: + uses: LegionIO/.github/.github/workflows/ci.yml@main From 9928cc021168f067e9a24fad50f61a542b43bfef Mon Sep 17 00:00:00 2001 From: Esity Date: Sat, 14 Mar 2026 17:54:51 -0500 Subject: [PATCH 07/22] reindex documentation to reflect current codebase state --- CLAUDE.md | 6 ++++-- README.md | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b2d5a04..dea57e6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,9 +13,9 @@ JSON wrapper module for the LegionIO framework. Wraps `multi_json` and `json_pur ## Architecture ``` -Legion::Json +Legion::JSON ├── .load(string, symbolize_keys: true) # Deserialize JSON -> Hash -├── .dump(hash) # Serialize Hash -> JSON +├── .dump(object, pretty: false) # Serialize Hash -> JSON ├── InvalidJson # Custom error class └── ParseError # JSON parse error class ``` @@ -46,6 +46,8 @@ Legion::Json **Foundation gem** - used by nearly every other Legion gem. `legion-settings` depends on it directly for config file parsing. All message serialization flows through this module. +Note: Inside the `Legion::` namespace, `JSON` refers to `Legion::JSON` — callers outside this gem must use `::JSON` to access the stdlib. + ``` legion-json ^ diff --git a/README.md b/README.md index 800291e..feed5ba 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,11 @@ gem 'legion-json' require 'legion/json' json_string = '{"foo":"bar","nested":{"hello":"world"}}' -Legion::Json.load(json_string) # => {foo: "bar", nested: {hello: "world"}} -Legion::Json.load(json_string, symbolize_keys: false) # => {"foo" => "bar", ...} +Legion::JSON.load(json_string) # => {foo: "bar", nested: {hello: "world"}} +Legion::JSON.load(json_string, symbolize_keys: false) # => {"foo" => "bar", ...} hash = { foo: 'bar', nested: { hello: 'world' } } -Legion::Json.dump(hash) # => '{"foo":"bar","nested":{"hello":"world"}}' +Legion::JSON.dump(hash) # => '{"foo":"bar","nested":{"hello":"world"}}' ``` Keys are symbolized by default, unlike standard Ruby JSON. From 8b90c2b403cdf67e3be5840f960ec9392a7f8143 Mon Sep 17 00:00:00 2001 From: Esity Date: Sat, 14 Mar 2026 20:46:25 -0500 Subject: [PATCH 08/22] trigger ci with updated shared workflow From 357162b25f60f7e7aed19a73d588a697e5120028 Mon Sep 17 00:00:00 2001 From: Esity Date: Sat, 14 Mar 2026 23:32:10 -0500 Subject: [PATCH 09/22] add release job to ci workflow runs after ci passes on push to main. calls reusable release workflow for version detection, github release, and rubygems publish. --- .github/workflows/ci.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a298d6b..c121a88 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,16 @@ name: CI -on: [push, pull_request] +on: + push: + branches: [main] + pull_request: + jobs: ci: uses: LegionIO/.github/.github/workflows/ci.yml@main + + release: + needs: ci + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: LegionIO/.github/.github/workflows/release.yml@main + secrets: + rubygems-api-key: ${{ secrets.RUBYGEMS_API_KEY }} From 10a847032435ce0da453357f0675188d28d39fd0 Mon Sep 17 00:00:00 2001 From: Esity Date: Wed, 18 Mar 2026 23:46:42 -0500 Subject: [PATCH 10/22] reindex documentation to reflect current codebase state --- CLAUDE.md | 1 + README.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index dea57e6..d287171 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,6 +8,7 @@ JSON wrapper module for the LegionIO framework. Wraps `multi_json` and `json_pure` to provide a consistent JSON interface across all Legion gems and extensions. Automatically uses faster C-extension JSON gems (like `oj`) when available. **GitHub**: https://github.com/LegionIO/legion-json +**Version**: 1.2.0 **License**: Apache-2.0 ## Architecture diff --git a/README.md b/README.md index feed5ba..1b0e42d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ JSON wrapper module for the [LegionIO](https://github.com/LegionIO/LegionIO) framework. Wraps `multi_json` and `json_pure` to provide a consistent JSON interface across all Legion gems and extensions. Automatically uses faster C-extension JSON gems (like `oj`) when available. +**Version**: 1.2.0 + ## Installation ```bash From bd19296ccde78d51bb4bf93511e06432142aa86e Mon Sep 17 00:00:00 2001 From: Esity Date: Sun, 22 Mar 2026 21:39:27 -0500 Subject: [PATCH 11/22] add Legion::JSON::Helper mixin for LEX extensions (v1.2.1) --- CHANGELOG.md | 5 ++++ lib/legion/json.rb | 1 + lib/legion/json/helper.rb | 15 ++++++++++++ lib/legion/json/version.rb | 2 +- spec/legion/json/helper_spec.rb | 41 +++++++++++++++++++++++++++++++++ 5 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 lib/legion/json/helper.rb create mode 100644 spec/legion/json/helper_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b6c55c..55882de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ # Legion::JSON +## [1.2.1] - 2026-03-22 + +### Added +- `Legion::JSON::Helper` mixin module with `json_load` and `json_dump` convenience methods for LEX extensions + ## v1.2.0 Moving from BitBucket to GitHub. All git history is reset from this point on \ No newline at end of file diff --git a/lib/legion/json.rb b/lib/legion/json.rb index cfdf79f..a62a4d1 100644 --- a/lib/legion/json.rb +++ b/lib/legion/json.rb @@ -5,6 +5,7 @@ require 'legion/json/invalid_json' require 'json' require 'multi_json' +require_relative 'json/helper' module Legion module JSON diff --git a/lib/legion/json/helper.rb b/lib/legion/json/helper.rb new file mode 100644 index 0000000..8d1c4b7 --- /dev/null +++ b/lib/legion/json/helper.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Legion + module JSON + module Helper + def json_load(string, symbolize_keys: true) + Legion::JSON.load(string, symbolize_keys: symbolize_keys) + end + + def json_dump(object, pretty: false) + Legion::JSON.dump(object, pretty: pretty) + end + end + end +end diff --git a/lib/legion/json/version.rb b/lib/legion/json/version.rb index 4c01d83..d6ff2fa 100644 --- a/lib/legion/json/version.rb +++ b/lib/legion/json/version.rb @@ -2,6 +2,6 @@ module Legion module Json - VERSION = '1.2.0' + VERSION = '1.2.1' end end diff --git a/spec/legion/json/helper_spec.rb b/spec/legion/json/helper_spec.rb new file mode 100644 index 0000000..71ca4ae --- /dev/null +++ b/spec/legion/json/helper_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Legion::JSON::Helper do + let(:test_class) do + Class.new do + include Legion::JSON::Helper + end + end + let(:instance) { test_class.new } + + describe '#json_load' do + it 'parses JSON with symbolized keys by default' do + result = instance.json_load('{"name":"test","value":42}') + expect(result).to eq(name: 'test', value: 42) + end + + it 'parses JSON with string keys when requested' do + result = instance.json_load('{"name":"test"}', symbolize_keys: false) + expect(result).to eq('name' => 'test') + end + + it 'raises ParseError on invalid JSON' do + expect { instance.json_load('not json') }.to raise_error(Legion::JSON::ParseError) + end + end + + describe '#json_dump' do + it 'serializes a hash to JSON' do + result = instance.json_dump({ name: 'test', value: 42 }) + parsed = instance.json_load(result) + expect(parsed).to eq(name: 'test', value: 42) + end + + it 'accepts pretty option' do + result = instance.json_dump({ name: 'test' }, pretty: true) + expect(result).to include("\n") + end + end +end From e2faf1ed6ff081fd59a64e2e9c43b6780335e907 Mon Sep 17 00:00:00 2001 From: Esity Date: Wed, 25 Mar 2026 02:41:33 -0500 Subject: [PATCH 12/22] add repo governance files (CODEOWNERS, dependabot, CI) --- .github/CODEOWNERS | 7 +++++++ .github/dependabot.yml | 18 ++++++++++++++++++ .github/workflows/ci.yml | 22 ++++++++++++++++++++-- 3 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .github/dependabot.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..775c880 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,7 @@ +# Auto-generated from team-config.yml +# Team: core +# +# To apply: scripts/apply-codeowners.sh legion-json + +* @LegionIO/maintainers +* @LegionIO/core diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..79ea87c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,18 @@ +version: 2 +updates: + - package-ecosystem: bundler + directory: / + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 5 + labels: + - "type:dependencies" + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 5 + labels: + - "type:dependencies" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c121a88..a83e3a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,14 +3,32 @@ on: push: branches: [main] pull_request: + schedule: + - cron: '0 9 * * 1' jobs: ci: uses: LegionIO/.github/.github/workflows/ci.yml@main + lint: + uses: LegionIO/.github/.github/workflows/lint-patterns.yml@main + + security: + uses: LegionIO/.github/.github/workflows/security-scan.yml@main + + version-changelog: + uses: LegionIO/.github/.github/workflows/version-changelog.yml@main + + dependency-review: + uses: LegionIO/.github/.github/workflows/dependency-review.yml@main + + stale: + if: github.event_name == 'schedule' + uses: LegionIO/.github/.github/workflows/stale.yml@main + release: - needs: ci + needs: [ci, lint] if: github.event_name == 'push' && github.ref == 'refs/heads/main' uses: LegionIO/.github/.github/workflows/release.yml@main secrets: - rubygems-api-key: ${{ secrets.RUBYGEMS_API_KEY }} + rubygems-api-key: ${{ secrets.RUBYGEMS_API_KEY }} \ No newline at end of file From b4a8a16390b11fa287c10d6a7b5fd9da51a02586 Mon Sep 17 00:00:00 2001 From: Esity Date: Thu, 26 Mar 2026 10:57:35 -0500 Subject: [PATCH 13/22] add parse, generate, pretty_generate, fast_generate methods (#2) --- CHANGELOG.md | 9 ++++ lib/legion/json.rb | 22 ++++++++ lib/legion/json/helper.rb | 12 +++++ lib/legion/json/version.rb | 2 +- spec/legion/json/helper_spec.rb | 35 +++++++++++++ spec/legion/json_spec.rb | 93 +++++++++++++++++++++++++++++++++ 6 files changed, 172 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55882de..137e06c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Legion::JSON +## [1.3.0] - 2026-03-26 + +### Added +- `.parse(string, symbolize_names: true)` — wraps `::JSON.parse` with `ParseError` error handling and symbol keys by default +- `.generate(object)` — wraps `::JSON.generate` for compact output +- `.pretty_generate(object)` — wraps `::JSON.pretty_generate` for formatted output +- `.fast_generate(object)` — wraps `::JSON.fast_generate` for unchecked fast output +- Helper methods: `json_parse`, `json_generate`, `json_pretty_generate` in `Legion::JSON::Helper` + ## [1.2.1] - 2026-03-22 ### Added diff --git a/lib/legion/json.rb b/lib/legion/json.rb index a62a4d1..486cd0a 100644 --- a/lib/legion/json.rb +++ b/lib/legion/json.rb @@ -25,5 +25,27 @@ def dump(object, pretty: false) parser.dump(object, pretty: pretty) end module_function :dump + + def parse(string, symbolize_names: true) + ::JSON.parse(string, symbolize_names: symbolize_names) + rescue ::JSON::ParserError => e + raise Legion::JSON::ParseError.build(e, string) + end + module_function :parse + + def generate(object) + ::JSON.generate(object) + end + module_function :generate + + def pretty_generate(object) + ::JSON.pretty_generate(object) + end + module_function :pretty_generate + + def fast_generate(object) + ::JSON.fast_generate(object) + end + module_function :fast_generate end end diff --git a/lib/legion/json/helper.rb b/lib/legion/json/helper.rb index 8d1c4b7..431b1c2 100644 --- a/lib/legion/json/helper.rb +++ b/lib/legion/json/helper.rb @@ -10,6 +10,18 @@ def json_load(string, symbolize_keys: true) def json_dump(object, pretty: false) Legion::JSON.dump(object, pretty: pretty) end + + def json_parse(string, symbolize_names: true) + Legion::JSON.parse(string, symbolize_names: symbolize_names) + end + + def json_generate(object) + Legion::JSON.generate(object) + end + + def json_pretty_generate(object) + Legion::JSON.pretty_generate(object) + end end end end diff --git a/lib/legion/json/version.rb b/lib/legion/json/version.rb index d6ff2fa..4b9b70b 100644 --- a/lib/legion/json/version.rb +++ b/lib/legion/json/version.rb @@ -2,6 +2,6 @@ module Legion module Json - VERSION = '1.2.1' + VERSION = '1.3.0' end end diff --git a/spec/legion/json/helper_spec.rb b/spec/legion/json/helper_spec.rb index 71ca4ae..2b05f16 100644 --- a/spec/legion/json/helper_spec.rb +++ b/spec/legion/json/helper_spec.rb @@ -38,4 +38,39 @@ expect(result).to include("\n") end end + + describe '#json_parse' do + it 'parses JSON with symbolized keys by default' do + result = instance.json_parse('{"name":"test"}') + expect(result).to eq(name: 'test') + end + + it 'returns string keys when requested' do + result = instance.json_parse('{"name":"test"}', symbolize_names: false) + expect(result).to eq('name' => 'test') + end + + it 'raises ParseError on invalid JSON' do + expect { instance.json_parse('not json') }.to raise_error(Legion::JSON::ParseError) + end + end + + describe '#json_generate' do + it 'serializes a hash to compact JSON' do + result = instance.json_generate({ name: 'test' }) + expect(result).to eq('{"name":"test"}') + end + end + + describe '#json_pretty_generate' do + it 'produces formatted output with newlines' do + result = instance.json_pretty_generate({ name: 'test' }) + expect(result).to include("\n") + end + + it 'produces valid JSON' do + result = instance.json_pretty_generate({ name: 'test' }) + expect(instance.json_parse(result)).to eq(name: 'test') + end + end end diff --git a/spec/legion/json_spec.rb b/spec/legion/json_spec.rb index 4a1d0ab..d3927bb 100644 --- a/spec/legion/json_spec.rb +++ b/spec/legion/json_spec.rb @@ -173,6 +173,99 @@ end end end + + describe '.parse' do + it 'parses JSON with symbolized keys by default' do + result = described_class.parse('{"foo":"bar"}') + expect(result).to eq(foo: 'bar') + end + + it 'parses nested objects with symbolized keys' do + result = described_class.parse('{"outer":{"inner":"value"}}') + expect(result[:outer][:inner]).to eq('value') + end + + it 'returns string keys when symbolize_names is false' do + result = described_class.parse('{"foo":"bar"}', symbolize_names: false) + expect(result).to eq('foo' => 'bar') + end + + it 'parses arrays' do + result = described_class.parse('[1,"two",3]') + expect(result).to eq([1, 'two', 3]) + end + + it 'raises ParseError on invalid JSON' do + expect { described_class.parse('{bad}') }.to raise_error(Legion::JSON::ParseError) + end + + it 'includes original input in ParseError' do + bad = '{"broken' + described_class.parse(bad) + rescue Legion::JSON::ParseError => e + expect(e.data).to eq(bad) + end + + it 'round-trips with generate' do + original = { name: 'test', count: 5 } + json = described_class.generate(original) + result = described_class.parse(json) + expect(result).to eq(original) + end + end + + describe '.generate' do + it 'serializes a hash to compact JSON' do + result = described_class.generate({ foo: 'bar' }) + expect(result).to eq('{"foo":"bar"}') + end + + it 'serializes an array' do + result = described_class.generate([1, 2, 3]) + expect(result).to eq('[1,2,3]') + end + + it 'produces compact output without newlines' do + result = described_class.generate({ a: 1, b: 2 }) + expect(result).not_to include("\n") + end + + it 'serializes nested structures' do + result = described_class.generate({ a: { b: [1, 2] } }) + parsed = described_class.parse(result) + expect(parsed).to eq(a: { b: [1, 2] }) + end + end + + describe '.pretty_generate' do + it 'produces formatted output with newlines' do + result = described_class.pretty_generate({ foo: 'bar' }) + expect(result).to include("\n") + end + + it 'produces valid JSON that round-trips' do + original = { name: 'test', items: [1, 2] } + pretty = described_class.pretty_generate(original) + expect(described_class.parse(pretty)).to eq(original) + end + + it 'produces indented output' do + result = described_class.pretty_generate({ foo: 'bar' }) + expect(result).to include(' ') + end + end + + describe '.fast_generate' do + it 'serializes a hash' do + result = described_class.fast_generate({ foo: 'bar' }) + expect(described_class.parse(result)).to eq(foo: 'bar') + end + + it 'serializes an array' do + result = described_class.fast_generate([1, 2, 3]) + expect(described_class.parse(result)).to eq([1, 2, 3]) + end + end end RSpec.describe Legion::JSON::ParseError do From 4f3411181b1e5837225a9bf0204b71b353322506 Mon Sep 17 00:00:00 2001 From: Esity Date: Thu, 26 Mar 2026 11:03:36 -0500 Subject: [PATCH 14/22] avoid api-json-dump-kwargs lint false positive in helper (#2) --- lib/legion/json/helper.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/legion/json/helper.rb b/lib/legion/json/helper.rb index 431b1c2..4b68a4d 100644 --- a/lib/legion/json/helper.rb +++ b/lib/legion/json/helper.rb @@ -8,7 +8,8 @@ def json_load(string, symbolize_keys: true) end def json_dump(object, pretty: false) - Legion::JSON.dump(object, pretty: pretty) + opts = { pretty: pretty } + Legion::JSON.dump(object, **opts) end def json_parse(string, symbolize_names: true) From d7819a27b91841a744df8d1769bc3db50d71b8b6 Mon Sep 17 00:00:00 2001 From: Esity Date: Thu, 26 Mar 2026 11:23:42 -0500 Subject: [PATCH 15/22] apply copilot review suggestions (#2) --- lib/legion/json.rb | 2 +- spec/legion/json_spec.rb | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/legion/json.rb b/lib/legion/json.rb index 486cd0a..bb680f5 100644 --- a/lib/legion/json.rb +++ b/lib/legion/json.rb @@ -28,7 +28,7 @@ def dump(object, pretty: false) def parse(string, symbolize_names: true) ::JSON.parse(string, symbolize_names: symbolize_names) - rescue ::JSON::ParserError => e + rescue StandardError => e raise Legion::JSON::ParseError.build(e, string) end module_function :parse diff --git a/spec/legion/json_spec.rb b/spec/legion/json_spec.rb index d3927bb..eb01a0c 100644 --- a/spec/legion/json_spec.rb +++ b/spec/legion/json_spec.rb @@ -201,9 +201,9 @@ it 'includes original input in ParseError' do bad = '{"broken' - described_class.parse(bad) - rescue Legion::JSON::ParseError => e - expect(e.data).to eq(bad) + expect { described_class.parse(bad) }.to raise_error(Legion::JSON::ParseError) do |e| + expect(e.data).to eq(bad) + end end it 'round-trips with generate' do From 51fbcee05f7a3e87d5928f20c5737ca58d9a46d2 Mon Sep 17 00:00:00 2001 From: Esity Date: Fri, 27 Mar 2026 09:06:11 -0500 Subject: [PATCH 16/22] accept **kwargs in dump for ergonomic keyword serialization --- CHANGELOG.md | 6 ++++++ lib/legion/json.rb | 5 +++-- lib/legion/json/version.rb | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 137e06c..21309d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Legion::JSON +## [1.3.1] - 2026-03-27 + +### Changed +- `.dump` now accepts `**kwargs` — callers can pass `Legion::JSON.dump(key: val)` without wrapping in `{}` +- `pretty:` keyword option preserved; all other kwargs become the serialized object + ## [1.3.0] - 2026-03-26 ### Added diff --git a/lib/legion/json.rb b/lib/legion/json.rb index bb680f5..5684bb7 100644 --- a/lib/legion/json.rb +++ b/lib/legion/json.rb @@ -21,8 +21,9 @@ def load(string, symbolize_keys: true) end module_function :load - def dump(object, pretty: false) - parser.dump(object, pretty: pretty) + def dump(object = nil, pretty: false, **kwargs) + data = object.nil? ? kwargs : object + parser.dump(data, pretty: pretty) end module_function :dump diff --git a/lib/legion/json/version.rb b/lib/legion/json/version.rb index 4b9b70b..13b1158 100644 --- a/lib/legion/json/version.rb +++ b/lib/legion/json/version.rb @@ -2,6 +2,6 @@ module Legion module Json - VERSION = '1.3.0' + VERSION = '1.3.1' end end From 683648df15a4d22a21949796b781542fa1ccd6eb Mon Sep 17 00:00:00 2001 From: Esity Date: Sun, 29 Mar 2026 00:55:32 -0500 Subject: [PATCH 17/22] update project documentation --- CLAUDE.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d287171..e4b67d6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ JSON wrapper module for the LegionIO framework. Wraps `multi_json` and `json_pure` to provide a consistent JSON interface across all Legion gems and extensions. Automatically uses faster C-extension JSON gems (like `oj`) when available. **GitHub**: https://github.com/LegionIO/legion-json -**Version**: 1.2.0 +**Version**: 1.2.1 **License**: Apache-2.0 ## Architecture diff --git a/README.md b/README.md index 1b0e42d..b4e3d07 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ JSON wrapper module for the [LegionIO](https://github.com/LegionIO/LegionIO) framework. Wraps `multi_json` and `json_pure` to provide a consistent JSON interface across all Legion gems and extensions. Automatically uses faster C-extension JSON gems (like `oj`) when available. -**Version**: 1.2.0 +**Version**: 1.2.1 ## Installation From af003e441a12ea670596bdc33f3961e2f8ed3669 Mon Sep 17 00:00:00 2001 From: Esity Date: Tue, 31 Mar 2026 18:53:11 -0500 Subject: [PATCH 18/22] clean up dev dependencies: add rubocop-legion --- Gemfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile b/Gemfile index f6c3759..4c18b66 100644 --- a/Gemfile +++ b/Gemfile @@ -8,5 +8,6 @@ group :test do gem 'rspec' gem 'rspec_junit_formatter' gem 'rubocop' + gem 'rubocop-legion' gem 'simplecov' end From a15333bcdca2ba029c0056d5ecca7a90ad4c7f10 Mon Sep 17 00:00:00 2001 From: Esity Date: Wed, 8 Apr 2026 00:22:32 -0500 Subject: [PATCH 19/22] add logging helper and structured exception handling to json parse/load - extend Legion::JSON with Legion::Logging::Helper - emit debug-level handle_exception log in .load and .parse before re-raising ParseError - bump version to 1.3.2 --- CHANGELOG.md | 6 ++++++ CLAUDE.md | 20 +++++++++++++------- README.md | 13 +++++++++++-- lib/legion/json.rb | 5 +++++ lib/legion/json/version.rb | 2 +- 5 files changed, 36 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21309d0..ffeacd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Legion::JSON +## [1.3.2] - 2026-04-08 + +### Changed +- `Legion::JSON` now extends `Legion::Logging::Helper` for structured exception logging +- Parse errors in `.load` and `.parse` now emit a debug-level log via `handle_exception` before re-raising `ParseError` + ## [1.3.1] - 2026-03-27 ### Changed diff --git a/CLAUDE.md b/CLAUDE.md index e4b67d6..1d8eb5a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,24 +8,30 @@ JSON wrapper module for the LegionIO framework. Wraps `multi_json` and `json_pure` to provide a consistent JSON interface across all Legion gems and extensions. Automatically uses faster C-extension JSON gems (like `oj`) when available. **GitHub**: https://github.com/LegionIO/legion-json -**Version**: 1.2.1 +**Version**: 1.3.1 **License**: Apache-2.0 ## Architecture ``` Legion::JSON -├── .load(string, symbolize_keys: true) # Deserialize JSON -> Hash -├── .dump(object, pretty: false) # Serialize Hash -> JSON -├── InvalidJson # Custom error class -└── ParseError # JSON parse error class +├── .load(string, symbolize_keys: true) # Deserialize JSON -> Hash (via MultiJson) +├── .dump(object = nil, pretty: false, **kwargs) # Serialize Hash -> JSON (via MultiJson); nil object uses kwargs +├── .parse(string, symbolize_names: true) # ::JSON.parse with symbolize_names (stdlib) +├── .generate(object) # ::JSON.generate (stdlib) +├── .pretty_generate(object) # ::JSON.pretty_generate (stdlib) +├── .fast_generate(object) # ::JSON.fast_generate (stdlib) +├── InvalidJson # Custom error class +└── ParseError # JSON parse error class ``` ### Key Design Patterns -- **Thin Wrapper**: Delegates to `MultiJson.load` / `MultiJson.dump` -- **Symbolized Keys by Default**: `symbolize_keys: true` is the default (unlike standard JSON) +- **Dual API**: `.load`/`.dump` route through MultiJson (adapter abstraction). `.parse`/`.generate`/`.pretty_generate`/`.fast_generate` route through Ruby stdlib `::JSON` directly. +- **Symbolized Keys by Default**: `symbolize_keys: true` is the default for `.load`; `symbolize_names: true` for `.parse` - **Auto C-Extension**: If `oj` gem is installed, `multi_json` automatically uses it for performance +- **Keyword Form for dump**: `.dump(pretty: true, foo: 'bar')` — when `object` is nil, `**kwargs` become the data +- **Namespace note**: Inside `module Legion`, bare `JSON` resolves to `Legion::JSON`. Use `::JSON` to access stdlib. ## Dependencies diff --git a/README.md b/README.md index b4e3d07..430e9d7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ JSON wrapper module for the [LegionIO](https://github.com/LegionIO/LegionIO) framework. Wraps `multi_json` and `json_pure` to provide a consistent JSON interface across all Legion gems and extensions. Automatically uses faster C-extension JSON gems (like `oj`) when available. -**Version**: 1.2.1 +**Version**: 1.3.1 ## Installation @@ -27,9 +27,18 @@ Legion::JSON.load(json_string, symbolize_keys: false) # => {"foo" => "bar", .. hash = { foo: 'bar', nested: { hello: 'world' } } Legion::JSON.dump(hash) # => '{"foo":"bar","nested":{"hello":"world"}}' +Legion::JSON.dump(pretty: true, foo: 'bar') # keyword form — object: nil uses kwargs as data + +# Full stdlib surface +Legion::JSON.parse(string) # ::JSON.parse with symbolize_names: true +Legion::JSON.generate(object) # ::JSON.generate +Legion::JSON.pretty_generate(object) # ::JSON.pretty_generate +Legion::JSON.fast_generate(object) # ::JSON.fast_generate ``` -Keys are symbolized by default, unlike standard Ruby JSON. +Keys are symbolized by default, unlike standard Ruby JSON. `load` delegates to `MultiJson`; `parse`/`generate`/`pretty_generate`/`fast_generate` delegate to Ruby's stdlib `::JSON`. + +Inside `module Legion`, `JSON` resolves to `Legion::JSON`. Use `::JSON` to access stdlib directly. ## Requirements diff --git a/lib/legion/json.rb b/lib/legion/json.rb index 5684bb7..79b7568 100644 --- a/lib/legion/json.rb +++ b/lib/legion/json.rb @@ -3,12 +3,15 @@ require 'legion/json/version' require 'legion/json/parse_error' require 'legion/json/invalid_json' +require 'legion/logging' require 'json' require 'multi_json' require_relative 'json/helper' module Legion module JSON + extend Legion::Logging::Helper + def parser @parser ||= MultiJson end @@ -17,6 +20,7 @@ def parser def load(string, symbolize_keys: true) parser.load(string, symbolize_keys: symbolize_keys) rescue StandardError => e + handle_exception(e, level: :debug, operation: :json_load, string_length: string.to_s.length) raise Legion::JSON::ParseError.build(e, string) end module_function :load @@ -30,6 +34,7 @@ def dump(object = nil, pretty: false, **kwargs) def parse(string, symbolize_names: true) ::JSON.parse(string, symbolize_names: symbolize_names) rescue StandardError => e + handle_exception(e, level: :debug, operation: :json_parse, string_length: string.to_s.length) raise Legion::JSON::ParseError.build(e, string) end module_function :parse diff --git a/lib/legion/json/version.rb b/lib/legion/json/version.rb index 13b1158..9e614f0 100644 --- a/lib/legion/json/version.rb +++ b/lib/legion/json/version.rb @@ -2,6 +2,6 @@ module Legion module Json - VERSION = '1.3.1' + VERSION = '1.3.2' end end From 6891262c2ac1043a7cc8b515f03e79bf804b9f64 Mon Sep 17 00:00:00 2001 From: Esity Date: Wed, 8 Apr 2026 00:41:43 -0500 Subject: [PATCH 20/22] fix broken require of legion-logging and restore simplecov coverage --- CHANGELOG.md | 6 +++--- lib/legion/json.rb | 5 ----- spec/legion/json_spec.rb | 2 -- spec/spec_helper.rb | 2 +- 4 files changed, 4 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffeacd8..2ede4b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,9 @@ ## [1.3.2] - 2026-04-08 -### Changed -- `Legion::JSON` now extends `Legion::Logging::Helper` for structured exception logging -- Parse errors in `.load` and `.parse` now emit a debug-level log via `handle_exception` before re-raising `ParseError` +### Fixed +- Removed `require 'legion/logging'` and `Legion::Logging::Helper` dependency that broke standalone usage (legion-logging is not a gemspec dependency) +- Fixed SimpleCov profile not being activated in spec_helper, restoring 100% coverage enforcement ## [1.3.1] - 2026-03-27 diff --git a/lib/legion/json.rb b/lib/legion/json.rb index 79b7568..5684bb7 100644 --- a/lib/legion/json.rb +++ b/lib/legion/json.rb @@ -3,15 +3,12 @@ require 'legion/json/version' require 'legion/json/parse_error' require 'legion/json/invalid_json' -require 'legion/logging' require 'json' require 'multi_json' require_relative 'json/helper' module Legion module JSON - extend Legion::Logging::Helper - def parser @parser ||= MultiJson end @@ -20,7 +17,6 @@ def parser def load(string, symbolize_keys: true) parser.load(string, symbolize_keys: symbolize_keys) rescue StandardError => e - handle_exception(e, level: :debug, operation: :json_load, string_length: string.to_s.length) raise Legion::JSON::ParseError.build(e, string) end module_function :load @@ -34,7 +30,6 @@ def dump(object = nil, pretty: false, **kwargs) def parse(string, symbolize_names: true) ::JSON.parse(string, symbolize_names: symbolize_names) rescue StandardError => e - handle_exception(e, level: :debug, operation: :json_parse, string_length: string.to_s.length) raise Legion::JSON::ParseError.build(e, string) end module_function :parse diff --git a/spec/legion/json_spec.rb b/spec/legion/json_spec.rb index eb01a0c..c8ac6a4 100644 --- a/spec/legion/json_spec.rb +++ b/spec/legion/json_spec.rb @@ -3,8 +3,6 @@ require 'spec_helper' require 'legion/json' -SimpleCov.command_name 'lib/legion/json' - RSpec.describe Legion::JSON do describe '.parser' do it 'returns MultiJson as the default parser' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0ddd9b0..fa0b481 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -9,7 +9,7 @@ add_filter '/tmp/' end - SimpleCov.start do + SimpleCov.start 'legion-json' do formatter SimpleCov::Formatter::SimpleFormatter if ENV.key? 'SONAR_TOKEN' end SimpleCov.use_merging(true) From a00b2a43258ec19ab02e14ea915c6a00e5f18a88 Mon Sep 17 00:00:00 2001 From: Esity Date: Wed, 8 Apr 2026 00:48:35 -0500 Subject: [PATCH 21/22] apply copilot review suggestions (#5) --- CLAUDE.md | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1d8eb5a..548252a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ JSON wrapper module for the LegionIO framework. Wraps `multi_json` and `json_pure` to provide a consistent JSON interface across all Legion gems and extensions. Automatically uses faster C-extension JSON gems (like `oj`) when available. **GitHub**: https://github.com/LegionIO/legion-json -**Version**: 1.3.1 +**Version**: 1.3.2 **License**: Apache-2.0 ## Architecture diff --git a/README.md b/README.md index 430e9d7..9aedeb2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ JSON wrapper module for the [LegionIO](https://github.com/LegionIO/LegionIO) framework. Wraps `multi_json` and `json_pure` to provide a consistent JSON interface across all Legion gems and extensions. Automatically uses faster C-extension JSON gems (like `oj`) when available. -**Version**: 1.3.1 +**Version**: 1.3.2 ## Installation @@ -30,7 +30,7 @@ Legion::JSON.dump(hash) # => '{"foo":"bar","nest Legion::JSON.dump(pretty: true, foo: 'bar') # keyword form — object: nil uses kwargs as data # Full stdlib surface -Legion::JSON.parse(string) # ::JSON.parse with symbolize_names: true +Legion::JSON.parse(json_string) # ::JSON.parse with symbolize_names: true Legion::JSON.generate(object) # ::JSON.generate Legion::JSON.pretty_generate(object) # ::JSON.pretty_generate Legion::JSON.fast_generate(object) # ::JSON.fast_generate From ef9d258627ba5ef27f7d457e65eb88fdbeef9c36 Mon Sep 17 00:00:00 2001 From: Esity Date: Mon, 13 Apr 2026 19:47:48 -0500 Subject: [PATCH 22/22] fix: dump with pretty: false produces compact output when Oj adapter is active Oj/MultiJson treats any explicit `pretty:` keyword (even false) as "enable pretty printing." Only pass `pretty: true` to the parser when requested; omit the keyword entirely otherwise. --- CHANGELOG.md | 5 +++++ lib/legion/json.rb | 3 ++- lib/legion/json/version.rb | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ede4b2..40a3784 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Legion::JSON +## [1.3.3] - 2026-04-14 + +### Fixed +- `Legion::JSON.dump` with `pretty: false` (the default) produced pretty-printed output when Oj adapter was active. Oj/MultiJson treats any explicit `pretty:` keyword (even `false`) as truthy. Fix: only pass `pretty: true` when requested; omit the keyword entirely otherwise. + ## [1.3.2] - 2026-04-08 ### Fixed diff --git a/lib/legion/json.rb b/lib/legion/json.rb index 5684bb7..ea804c4 100644 --- a/lib/legion/json.rb +++ b/lib/legion/json.rb @@ -23,7 +23,8 @@ def load(string, symbolize_keys: true) def dump(object = nil, pretty: false, **kwargs) data = object.nil? ? kwargs : object - parser.dump(data, pretty: pretty) + # Only pass pretty: when true — Oj/MultiJson treats any explicit pretty: (even false) as truthy + pretty ? parser.dump(data, pretty: true) : parser.dump(data) end module_function :dump diff --git a/lib/legion/json/version.rb b/lib/legion/json/version.rb index 9e614f0..05cae11 100644 --- a/lib/legion/json/version.rb +++ b/lib/legion/json/version.rb @@ -2,6 +2,6 @@ module Legion module Json - VERSION = '1.3.2' + VERSION = '1.3.3' end end