Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into jruby_optz
Browse files Browse the repository at this point in the history
  • Loading branch information
headius committed Feb 6, 2025
2 parents 67a00da + c84daef commit 65d7801
Show file tree
Hide file tree
Showing 30 changed files with 1,929 additions and 4,763 deletions.
1 change: 0 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
ext/json/ext/parser/parser.c linguist-generated=true
java/src/json/ext/Parser.java linguist-generated=true
11 changes: 6 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ jobs:
fail-fast: false
matrix:
os:
- ubuntu-22.04
- macos-13
- ubuntu-latest
- macos-14
- windows-latest
ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }}
include:
- { os: ubuntu-24.04-arm, ruby: 3.4 }
- { os: macos-13, ruby: 3.4 }
- { os: windows-latest , ruby: mswin } # ruby/ruby windows CI
- { os: ubuntu-latest , ruby: jruby-9.4 } # Ruby 3.1
- { os: macos-latest , ruby: truffleruby-head }
Expand All @@ -41,8 +42,8 @@ jobs:
uses: ruby/setup-ruby-pkgs@v1
with:
ruby-version: ${{ matrix.ruby }}
apt-get: ragel
brew: ragel
apt-get: "${{ startsWith(matrix.ruby, 'jruby') && 'ragel' || '' }}"
brew: "${{ startsWith(matrix.ruby, 'jruby') && 'ragel' || '' }}"

- run: |
bundle config --without benchmark
Expand Down Expand Up @@ -70,7 +71,7 @@ jobs:
uses: ruby/setup-ruby-pkgs@v1
with:
ruby-version: "3.3"
apt-get: ragel valgrind
apt-get: valgrind

- run: |
bundle config --without benchmark
Expand Down
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changes

* `strict: true` now accept symbols as values. Previously they'd only be accepted as hash keys.
* The C extension Parser has been entirely reimplemented from scratch.
* Introduced `JSON::Coder` as a new API allowing to customize how non native types are serialized in a non-global way.

### 2024-12-18 (2.9.1)

* Fix support for Solaris 10.
Expand Down
52 changes: 0 additions & 52 deletions LEGAL
Original file line number Diff line number Diff line change
Expand Up @@ -6,55 +6,3 @@
All the files in this distribution are covered under either the Ruby's
license (see the file COPYING) or public-domain except some files
mentioned below.

== MIT License
>>>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

== Old-style BSD license
>>>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the University nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.

IMPORTANT NOTE::

From ftp://ftp.cs.berkeley.edu/pub/4bsd/README.Impt.License.Change
paragraph 3 above is now null and void.
77 changes: 75 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ If bundler is not being used to manage dependencies, install the gem by executin

$ gem install json

## Usage
## Basic Usage

To use JSON you can

Expand All @@ -52,7 +52,80 @@ You can also use the `pretty_generate` method (which formats the output more
verbosely and nicely) or `fast_generate` (which doesn't do any of the security
checks generate performs, e. g. nesting deepness checks).

## Handling arbitrary types
## Casting non native types

JSON documents can only support Hashes, Arrays, Strings, Integers and Floats.

By default if you attempt to serialize something else, `JSON.generate` will
search for a `#to_json` method on that object:

```ruby
Position = Struct.new(:latitude, :longitude) do
def to_json(state = nil, *)
JSON::State.from_state(state).generate({
latitude: latitude,
longitude: longitude,
})
end
end

JSON.generate([
Position.new(12323.234, 435345.233),
Position.new(23434.676, 159435.324),
]) # => [{"latitude":12323.234,"longitude":435345.233},{"latitude":23434.676,"longitude":159435.324}]
```

If a `#to_json` method isn't defined on the object, `JSON.generate` will fallback to call `#to_s`:

```ruby
JSON.generate(Object.new) # => "#<Object:0x000000011e768b98>"
```

Both of these behavior can be disabled using the `strict: true` option:

```ruby
JSON.generate(Object.new, strict: true) # => Object not allowed in JSON (JSON::GeneratorError)
JSON.generate(Position.new(1, 2)) # => Position not allowed in JSON (JSON::GeneratorError)
```

## JSON::Coder

Since `#to_json` methods are global, it can sometimes be problematic if you need a given type to be
serialized in different ways in different locations.

Instead it is recommended to use the newer `JSON::Coder` API:

```ruby
module MyApp
API_JSON_CODER = JSON::Coder.new do |object|
case object
when Time
object.iso8601(3)
else
object
end
end
end

puts MyApp::API_JSON_CODER.dump(Time.now.utc) # => "2025-01-21T08:41:44.286Z"
```

The provided block is called for all objects that don't have a native JSON equivalent, and
must return a Ruby object that has a native JSON equivalent.

## Combining JSON fragments

To combine JSON fragments into a bigger JSON document, you can use `JSON::Fragment`:

```ruby
posts_json = cache.fetch_multi(post_ids) do |post_id|
JSON.generate(Post.find(post_id))
end
posts_json.map! { |post_json| JSON::Fragment.new(post_json) }
JSON.generate({ posts: posts_json, count: posts_json.count })
```

## Round-tripping arbitrary types

> [!CAUTION]
> You should never use `JSON.unsafe_load` nor `JSON.parse(str, create_additions: true)` to parse untrusted user input,
Expand Down
144 changes: 17 additions & 127 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -1,52 +1,30 @@
begin
require 'rubygems/package_task'
rescue LoadError
end
require "bundler/gem_tasks"

require 'rbconfig'
include RbConfig

require 'rake/clean'
CLOBBER.include 'doc', 'Gemfile.lock'
CLEAN.include FileList['diagrams/*.*'], 'doc', 'coverage', 'tmp',
FileList["ext/**/{Makefile,mkmf.log}"], 'build', 'dist', FileList['**/*.rbc'],
FileList["{ext,lib}/**/*.{so,bundle,#{CONFIG['DLEXT']},o,obj,pdb,lib,manifest,exp,def,jar,class,dSYM}"],
FileList['java/src/**/*.class']

require 'rake/testtask'
class UndocumentedTestTask < Rake::TestTask
def desc(*) end
end

which = lambda { |c|
w = `which #{c}`
break w.chomp unless w.empty?
}

MAKE = ENV['MAKE'] || %w[gmake make].find(&which)
BUNDLE = ENV['BUNDLE'] || %w[bundle].find(&which)

PKG_VERSION = File.foreach(File.join(__dir__, "lib/json/version.rb")) do |line|
/^\s*VERSION\s*=\s*'(.*)'/ =~ line and break $1
end rescue nil

EXT_ROOT_DIR = 'ext/json/ext'
EXT_PARSER_DIR = "#{EXT_ROOT_DIR}/parser"
EXT_PARSER_DL = "#{EXT_PARSER_DIR}/parser.#{CONFIG['DLEXT']}"
RAGEL_PATH = "#{EXT_PARSER_DIR}/parser.rl"
EXT_PARSER_SRC = "#{EXT_PARSER_DIR}/parser.c"
EXT_GENERATOR_DIR = "#{EXT_ROOT_DIR}/generator"
EXT_GENERATOR_DL = "#{EXT_GENERATOR_DIR}/generator.#{CONFIG['DLEXT']}"
EXT_GENERATOR_SRC = "#{EXT_GENERATOR_DIR}/generator.c"

JAVA_DIR = "java/src/json/ext"
JAVA_RAGEL_PATH = "#{JAVA_DIR}/Parser.rl"
JAVA_PARSER_SRC = "#{JAVA_DIR}/Parser.java"
JAVA_RAGEL_PATH = "#{JAVA_DIR}/ParserConfig.rl"
JAVA_PARSER_SRC = "#{JAVA_DIR}/ParserConfig.java"
JAVA_SOURCES = FileList["#{JAVA_DIR}/*.java"]
JAVA_CLASSES = []
JRUBY_PARSER_JAR = File.expand_path("lib/json/ext/parser.jar")
JRUBY_GENERATOR_JAR = File.expand_path("lib/json/ext/generator.jar")

which = lambda { |c|
w = `which #{c}`
break w.chomp unless w.empty?
}

if RUBY_PLATFORM =~ /mingw|mswin/
# cleans up Windows CI output
RAGEL_CODEGEN = %w[ragel].find(&which)
Expand All @@ -56,60 +34,18 @@ else
RAGEL_DOTGEN = %w[rlgen-dot rlgen-cd ragel].find(&which)
end

desc "Installing library (extension)"
task :install => [ :compile ] do
sitearchdir = CONFIG["sitearchdir"]
cd 'ext' do
for file in Dir["json/ext/*.#{CONFIG['DLEXT']}"]
d = File.join(sitearchdir, file)
mkdir_p File.dirname(d)
install(file, d)
end
warn " *** Installed EXT ruby library."
end
end

namespace :gems do
desc 'Install all development gems'
task :install do
sh "#{BUNDLE}"
end
end

file EXT_PARSER_DL => EXT_PARSER_SRC do
cd EXT_PARSER_DIR do
ruby 'extconf.rb'
sh MAKE
end
cp "#{EXT_PARSER_DIR}/parser.#{CONFIG['DLEXT']}", EXT_ROOT_DIR
end

file EXT_GENERATOR_DL => EXT_GENERATOR_SRC do
cd EXT_GENERATOR_DIR do
ruby 'extconf.rb'
sh MAKE
end
cp "#{EXT_GENERATOR_DIR}/generator.#{CONFIG['DLEXT']}", EXT_ROOT_DIR
end

file JAVA_PARSER_SRC => JAVA_RAGEL_PATH do
cd JAVA_DIR do
if RAGEL_CODEGEN == 'ragel'
sh "ragel Parser.rl -J -o Parser.java"
sh "ragel ParserConfig.rl -J -o ParserConfig.java"
else
sh "ragel -x Parser.rl | #{RAGEL_CODEGEN} -J"
sh "ragel -x ParserConfig.rl | #{RAGEL_CODEGEN} -J"
end
end
end

desc "Generate parser with ragel"
task :ragel => [EXT_PARSER_SRC, JAVA_PARSER_SRC]

desc "Delete the ragel generated C source"
task :ragel_clean do
rm_rf EXT_PARSER_SRC
rm_rf JAVA_PARSER_SRC
end
task :ragel => [JAVA_PARSER_SRC]

if defined?(RUBY_ENGINE) and RUBY_ENGINE == 'jruby'
ENV['JAVA_HOME'] ||= [
Expand Down Expand Up @@ -202,13 +138,14 @@ if defined?(RUBY_ENGINE) and RUBY_ENGINE == 'jruby'

task :release => :build
else
desc "Compiling extension"
if RUBY_ENGINE == 'truffleruby'
task :compile => [ :ragel, EXT_PARSER_DL ]
else
task :compile => [ :ragel, EXT_PARSER_DL, EXT_GENERATOR_DL ]
require 'rake/extensiontask'

unless RUBY_ENGINE == 'truffleruby'
Rake::ExtensionTask.new("json/ext/generator")
end

Rake::ExtensionTask.new("json/ext/parser")

UndocumentedTestTask.new do |t|
t.name = :test
t.test_files = FileList['test/json/*_test.rb']
Expand All @@ -234,53 +171,6 @@ else
system 'ctags', *Dir['**/*.{rb,c,h,java}']
end

file EXT_PARSER_SRC => RAGEL_PATH do
cd EXT_PARSER_DIR do
if RAGEL_CODEGEN == 'ragel'
sh "ragel parser.rl -G2 -o parser.c"
else
sh "ragel -x parser.rl | #{RAGEL_CODEGEN} -G2"
end
src = File.read("parser.c").gsub(/[ \t]+$/, '')
src.gsub!(/^static const int (JSON_.*=.*);$/, 'enum {\1};')
src.gsub!(/^(static const char) (_JSON(?:_\w+)?_nfa_\w+)(?=\[\] =)/, '\1 MAYBE_UNUSED(\2)')
src.gsub!(/0 <= ([\( ]+\*[\( ]*p\)+) && \1 <= 31/, "0 <= (signed char)(*(p)) && (*(p)) <= 31")
src[0, 0] = "/* This file is automatically generated from parser.rl by using ragel */"
File.open("parser.c", "w") {|f| f.print src}
end
end

desc "Generate diagrams of ragel parser (ps)"
task :ragel_dot_ps do
root = 'diagrams'
specs = []
File.new(RAGEL_PATH).grep(/^\s*machine\s*(\S+);\s*$/) { specs << $1 }
for s in specs
if RAGEL_DOTGEN == 'ragel'
sh "ragel #{RAGEL_PATH} -S#{s} -p -V | dot -Tps -o#{root}/#{s}.ps"
else
sh "ragel -x #{RAGEL_PATH} -S#{s} | #{RAGEL_DOTGEN} -p|dot -Tps -o#{root}/#{s}.ps"
end
end
end

desc "Generate diagrams of ragel parser (png)"
task :ragel_dot_png do
root = 'diagrams'
specs = []
File.new(RAGEL_PATH).grep(/^\s*machine\s*(\S+);\s*$/) { specs << $1 }
for s in specs
if RAGEL_DOTGEN == 'ragel'
sh "ragel #{RAGEL_PATH} -S#{s} -p -V | dot -Tpng -o#{root}/#{s}.png"
else
sh "ragel -x #{RAGEL_PATH} -S#{s} | #{RAGEL_DOTGEN} -p|dot -Tpng -o#{root}/#{s}.png"
end
end
end

desc "Generate diagrams of ragel parser"
task :ragel_dot => [ :ragel_dot_png, :ragel_dot_ps ]

desc "Create the gem packages"
task :package do
sh "gem build json.gemspec"
Expand Down
Loading

0 comments on commit 65d7801

Please sign in to comment.