emp_json 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rubocop.yml +24 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +1 -0
- data/Gemfile +18 -0
- data/Gemfile.lock +62 -0
- data/LICENSE.txt +21 -0
- data/README.md +33 -0
- data/Rakefile +16 -0
- data/emp-json-rb.gemspec +41 -0
- data/lib/empathy/emp_json/helpers/primitives.rb +127 -0
- data/lib/empathy/emp_json/helpers/rdf_list.rb +29 -0
- data/lib/empathy/emp_json/helpers/slices.rb +90 -0
- data/lib/empathy/emp_json/helpers/values.rb +58 -0
- data/lib/empathy/emp_json/serializer/fields.rb +102 -0
- data/lib/empathy/emp_json/serializer/inclusion.rb +59 -0
- data/lib/empathy/emp_json/serializer/records.rb +36 -0
- data/lib/empathy/emp_json/serializer/sequence.rb +37 -0
- data/lib/empathy/emp_json/serializer/serializing.rb +58 -0
- data/lib/empathy/emp_json/serializer.rb +45 -0
- data/lib/empathy/emp_json/version.rb +7 -0
- data/lib/empathy/emp_json.rb +17 -0
- data/sig/emp/json/rb.rbs +8 -0
- metadata +70 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9c980a0bd41e0236610d10af6521e6e41ed39329973c51acdaf48a5567e6162a
|
4
|
+
data.tar.gz: a260ad775292dfbdc03c25a2d9472164687b918d73f85af4706a1cad7e31f367
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2b7c38569fc8700ea138d1fb0fc6dfb82caee3b8c3e805f50d5c06a7912f1bba3233c568980b4614d9d8ac4477ced6fb07dfdb3482fa00ec4eafbb69722500af
|
7
|
+
data.tar.gz: 591b643b63e2d0e497c7ba8c0989518a357b34d0b3d36b04b72872d9c11637bf5a28215aeb23f779ba5066c939bd72ad680d7e68d3d00c1b70d0dc27fb5c92e3
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 2.7
|
3
|
+
NewCops: enable
|
4
|
+
|
5
|
+
|
6
|
+
Style/StringLiterals:
|
7
|
+
Enabled: true
|
8
|
+
EnforcedStyle: double_quotes
|
9
|
+
|
10
|
+
Style/StringLiteralsInInterpolation:
|
11
|
+
Enabled: true
|
12
|
+
EnforcedStyle: double_quotes
|
13
|
+
|
14
|
+
Metrics/PerceivedComplexity:
|
15
|
+
Max: 10
|
16
|
+
|
17
|
+
Metrics/CyclomaticComplexity:
|
18
|
+
Max: 10
|
19
|
+
|
20
|
+
Metrics/MethodLength:
|
21
|
+
Max: 20
|
22
|
+
|
23
|
+
Layout/LineLength:
|
24
|
+
Max: 120
|
data/CHANGELOG.md
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Be civil
|
data/Gemfile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
# Specify your gem's dependencies in emp_json_rb.gemspec
|
6
|
+
gemspec
|
7
|
+
|
8
|
+
gem "rake", "~> 13.0"
|
9
|
+
|
10
|
+
gem "minitest", "~> 5.0"
|
11
|
+
|
12
|
+
gem "rubocop", "~> 1.21"
|
13
|
+
|
14
|
+
gem "activemodel", "~> 7"
|
15
|
+
|
16
|
+
gem "activesupport", "~> 7"
|
17
|
+
|
18
|
+
gem "rdf"
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
emp_json (1.1.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
activemodel (7.0.3)
|
10
|
+
activesupport (= 7.0.3)
|
11
|
+
activesupport (7.0.3)
|
12
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
13
|
+
i18n (>= 1.6, < 2)
|
14
|
+
minitest (>= 5.1)
|
15
|
+
tzinfo (~> 2.0)
|
16
|
+
ast (2.4.2)
|
17
|
+
concurrent-ruby (1.1.10)
|
18
|
+
i18n (1.10.0)
|
19
|
+
concurrent-ruby (~> 1.0)
|
20
|
+
link_header (0.0.8)
|
21
|
+
minitest (5.15.0)
|
22
|
+
parallel (1.22.1)
|
23
|
+
parser (3.1.2.0)
|
24
|
+
ast (~> 2.4.1)
|
25
|
+
rainbow (3.1.1)
|
26
|
+
rake (13.0.6)
|
27
|
+
rdf (3.2.7)
|
28
|
+
link_header (~> 0.0, >= 0.0.8)
|
29
|
+
regexp_parser (2.3.1)
|
30
|
+
rexml (3.2.5)
|
31
|
+
rubocop (1.29.0)
|
32
|
+
parallel (~> 1.10)
|
33
|
+
parser (>= 3.1.0.0)
|
34
|
+
rainbow (>= 2.2.2, < 4.0)
|
35
|
+
regexp_parser (>= 1.8, < 3.0)
|
36
|
+
rexml (>= 3.2.5, < 4.0)
|
37
|
+
rubocop-ast (>= 1.17.0, < 2.0)
|
38
|
+
ruby-progressbar (~> 1.7)
|
39
|
+
unicode-display_width (>= 1.4.0, < 3.0)
|
40
|
+
rubocop-ast (1.17.0)
|
41
|
+
parser (>= 3.1.1.0)
|
42
|
+
ruby-progressbar (1.11.0)
|
43
|
+
tzinfo (2.0.4)
|
44
|
+
concurrent-ruby (~> 1.0)
|
45
|
+
unicode-display_width (2.1.0)
|
46
|
+
|
47
|
+
PLATFORMS
|
48
|
+
arm64-darwin-21
|
49
|
+
ruby
|
50
|
+
x86_64-linux
|
51
|
+
|
52
|
+
DEPENDENCIES
|
53
|
+
activemodel (~> 7)
|
54
|
+
activesupport (~> 7)
|
55
|
+
emp_json!
|
56
|
+
minitest (~> 5.0)
|
57
|
+
rake (~> 13.0)
|
58
|
+
rdf
|
59
|
+
rubocop (~> 1.21)
|
60
|
+
|
61
|
+
BUNDLED WITH
|
62
|
+
2.3.9
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2022 Thom van Kalkeren
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# Empathy::Json
|
2
|
+
|
3
|
+
A gem to serialize and manipulate [EmpJson](https://empathy.tools/specifications/emp-json) slices.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Install the gem and add to the application's Gemfile by executing:
|
8
|
+
|
9
|
+
$ bundle add emp_json_rb
|
10
|
+
|
11
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
12
|
+
|
13
|
+
$ gem install emp_json_rb
|
14
|
+
|
15
|
+
## Usage
|
16
|
+
|
17
|
+
The module was built to work with [rdf-serializers](https://github.com/ontola/rdf-serializers), and uses serializer descriptions from that gem.
|
18
|
+
|
19
|
+
Include the `Empathy::Json::Slices` module and call `emp_json_hash(resource)` for a hash representation or `render_emp_json(resource)` for a stringified version.
|
20
|
+
|
21
|
+
## Development
|
22
|
+
|
23
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
24
|
+
|
25
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
26
|
+
|
27
|
+
## Contributing
|
28
|
+
|
29
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/empathy-tools/emp_json_rb. Contributors are expected to adhere to the [code of conduct](https://github.com/empathy-tools/emp_json_rb/blob/main/CODE_OF_CONDUCT.md).
|
30
|
+
|
31
|
+
## License
|
32
|
+
|
33
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rake/testtask"
|
5
|
+
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
7
|
+
t.libs << "test"
|
8
|
+
t.libs << "lib"
|
9
|
+
t.test_files = FileList["test/**/*.rb"]
|
10
|
+
end
|
11
|
+
|
12
|
+
require "rubocop/rake_task"
|
13
|
+
|
14
|
+
RuboCop::RakeTask.new
|
15
|
+
|
16
|
+
task default: %i[test rubocop]
|
data/emp-json-rb.gemspec
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/empathy/emp_json/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "emp_json"
|
7
|
+
spec.version = Empathy::EmpJson::VERSION
|
8
|
+
spec.authors = ["Thom van Kalkeren"]
|
9
|
+
spec.email = ["thom@ontola.io"]
|
10
|
+
|
11
|
+
spec.summary = "Serializer for the EmpJson specification."
|
12
|
+
spec.description = "Serializer for the EmpJson specification."
|
13
|
+
spec.homepage = "https://empathy.tools/tools/emp-json"
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = ">= 2.7.0.preview1"
|
16
|
+
|
17
|
+
# spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
|
18
|
+
|
19
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
20
|
+
spec.metadata["source_code_uri"] = "https://github.com/empathy-tools/emp_json_rb"
|
21
|
+
spec.metadata["bug_tracker_uri"] = "https://github.com/empathy-tools/emp_json_rb/issues"
|
22
|
+
spec.metadata["changelog_uri"] = "https://github.com/empathy-tools/emp_json_rb/CHANGELOG.md"
|
23
|
+
|
24
|
+
# Specify which files should be added to the gem when it is released.
|
25
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
26
|
+
spec.files = Dir.chdir(__dir__) do
|
27
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
28
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
29
|
+
end
|
30
|
+
end
|
31
|
+
spec.bindir = "exe"
|
32
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
33
|
+
spec.require_paths = ["lib"]
|
34
|
+
|
35
|
+
# Uncomment to register a new dependency of your gem
|
36
|
+
# spec.add_dependency "example-gem", "~> 1.0"
|
37
|
+
|
38
|
+
# For more information and examples about making a new gem, check out our
|
39
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
40
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
41
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/time_with_zone"
|
4
|
+
|
5
|
+
module Empathy
|
6
|
+
module EmpJson
|
7
|
+
module Helpers
|
8
|
+
# Functions relating to serializing primitives.
|
9
|
+
module Primitives # rubocop:disable Metrics/ModuleLength
|
10
|
+
EMP_TYPE_GLOBAL_ID = "id"
|
11
|
+
EMP_TYPE_LOCAL_ID = "lid"
|
12
|
+
EMP_TYPE_DATETIME = "dt"
|
13
|
+
EMP_TYPE_STRING = "s"
|
14
|
+
EMP_TYPE_BOOL = "b"
|
15
|
+
EMP_TYPE_INTEGER = "i"
|
16
|
+
EMP_TYPE_LONG = "l"
|
17
|
+
EMP_TYPE_PRIMITIVE = "p"
|
18
|
+
EMP_TYPE_LANGSTRING = "ls"
|
19
|
+
|
20
|
+
RDF_RDFV = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
21
|
+
RDF_RDFV_LANGSTRING = "#{RDF_RDFV}langString"
|
22
|
+
RDF_RDFS = "http://www.w3.org/2000/01/rdf-schema#"
|
23
|
+
RDF_XSD = "http://www.w3.org/2001/XMLSchema#"
|
24
|
+
RDF_XSD_TOKEN = "#{RDF_XSD}token"
|
25
|
+
RDF_XSD_STRING = "#{RDF_XSD}string"
|
26
|
+
RDF_XSD_DATETIME = "#{RDF_XSD}dateTime"
|
27
|
+
RDF_XSD_BOOLEAN = "#{RDF_XSD}boolean"
|
28
|
+
RDF_XSD_INTEGER = "#{RDF_XSD}integer"
|
29
|
+
|
30
|
+
def object_to_value(value)
|
31
|
+
return primitive_to_value(value.iri) if value.respond_to?(:iri)
|
32
|
+
|
33
|
+
return primitive_to_value(value.subject) if value.is_a?(RDF::List)
|
34
|
+
|
35
|
+
primitive_to_value(value)
|
36
|
+
end
|
37
|
+
|
38
|
+
def node_to_local_id(value)
|
39
|
+
shorthand(EMP_TYPE_LOCAL_ID, "_:#{value.id}")
|
40
|
+
end
|
41
|
+
|
42
|
+
def primitive_to_value(value) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize
|
43
|
+
throw_unknown_ruby_object(value) if value.nil?
|
44
|
+
|
45
|
+
case value
|
46
|
+
when RDF::Node
|
47
|
+
node_to_local_id(value)
|
48
|
+
when RDF::URI, URI
|
49
|
+
shorthand(EMP_TYPE_GLOBAL_ID, value.to_s)
|
50
|
+
when DateTime, ActiveSupport::TimeWithZone
|
51
|
+
shorthand(EMP_TYPE_DATETIME, value.iso8601)
|
52
|
+
when String
|
53
|
+
shorthand(EMP_TYPE_STRING, value)
|
54
|
+
when true, false
|
55
|
+
shorthand(EMP_TYPE_BOOL, value.to_s)
|
56
|
+
when Symbol
|
57
|
+
primitive(RDF_XSD_TOKEN, value.to_s)
|
58
|
+
when Integer
|
59
|
+
integer_to_value(value)
|
60
|
+
when Float, Numeric
|
61
|
+
use_rdf_rb_for_primitive(value)
|
62
|
+
when RDF::Literal
|
63
|
+
rdf_literal_to_value(value)
|
64
|
+
else
|
65
|
+
throw_unknown_ruby_object(value)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def integer_to_value(value)
|
70
|
+
size = value.bit_length
|
71
|
+
if size <= 32
|
72
|
+
shorthand(EMP_TYPE_INTEGER, value.to_s)
|
73
|
+
elsif size > 32 && size <= 64
|
74
|
+
shorthand(EMP_TYPE_LONG, value.to_s)
|
75
|
+
else
|
76
|
+
use_rdf_rb_for_primitive(value)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def rdf_literal_to_value(value)
|
81
|
+
case value.datatype.to_s
|
82
|
+
when RDF_RDFV_LANGSTRING
|
83
|
+
{
|
84
|
+
type: EMP_TYPE_LANGSTRING,
|
85
|
+
l: value.language.to_s,
|
86
|
+
v: value.value
|
87
|
+
}
|
88
|
+
when RDF_XSD_STRING
|
89
|
+
shorthand(EMP_TYPE_STRING, value.value)
|
90
|
+
when RDF_XSD_DATETIME
|
91
|
+
shorthand(EMP_TYPE_DATETIME, value.value)
|
92
|
+
when RDF_XSD_BOOLEAN
|
93
|
+
shorthand(EMP_TYPE_BOOL, value.value)
|
94
|
+
when RDF_XSD_INTEGER
|
95
|
+
integer_to_value(value.to_i)
|
96
|
+
else
|
97
|
+
throw "unknown RDF::Literal"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def use_rdf_rb_for_primitive(value)
|
102
|
+
rdf = RDF::Literal(value)
|
103
|
+
primitive(rdf.datatype.to_s, rdf.value)
|
104
|
+
end
|
105
|
+
|
106
|
+
def shorthand(type, value)
|
107
|
+
{
|
108
|
+
type: type,
|
109
|
+
v: value
|
110
|
+
}
|
111
|
+
end
|
112
|
+
|
113
|
+
def primitive(datatype, value)
|
114
|
+
{
|
115
|
+
type: EMP_TYPE_PRIMITIVE,
|
116
|
+
dt: datatype,
|
117
|
+
v: value
|
118
|
+
}
|
119
|
+
end
|
120
|
+
|
121
|
+
def throw_unknown_ruby_object(value)
|
122
|
+
throw "unknown ruby object: #{value} (#{value.class})"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Empathy
|
4
|
+
module EmpJson
|
5
|
+
module Helpers
|
6
|
+
# Functions relating to serializing RDF::List records
|
7
|
+
module RDFList
|
8
|
+
def add_rdf_list_to_slice(slice, symbolize, **options)
|
9
|
+
elem = options.delete(:resource)
|
10
|
+
loop do
|
11
|
+
list_item_to_record(slice, elem, symbolize)
|
12
|
+
|
13
|
+
break if elem.rest_subject == NS.rdfv.nil
|
14
|
+
|
15
|
+
elem = elem.rest
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def list_item_to_record(slice, elem, symbolize) # rubocop:disable Metrics/AbcSize
|
20
|
+
rid = add_record_to_slice(slice, elem)
|
21
|
+
add_attribute_to_record(slice, rid, NS.rdfv.type, NS.rdfv.List, symbolize)
|
22
|
+
first = elem.first.is_a?(RDF::Term) ? elem.first : retrieve_id(elem.first)
|
23
|
+
add_attribute_to_record(slice, rid, NS.rdfv.first, first, symbolize)
|
24
|
+
add_attribute_to_record(slice, rid, NS.rdfv.rest, elem.rest_subject, symbolize)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/hash/indifferent_access"
|
4
|
+
|
5
|
+
module Empathy
|
6
|
+
module EmpJson
|
7
|
+
module Helpers
|
8
|
+
# Additional functions for working with slices
|
9
|
+
module Slices
|
10
|
+
# Retrieves all values for the given [field].
|
11
|
+
def all_values_from_slice(slice, field, _website_iri = nil)
|
12
|
+
slice
|
13
|
+
.values
|
14
|
+
.flat_map { |record| field_from_record(record, field) }
|
15
|
+
.map { |values| normalise_slice_values(values) }
|
16
|
+
.compact
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_record_to_slice(slice, resource)
|
20
|
+
id = resource.is_a?(RDF::Resource) ? resource : retrieve_id(resource)
|
21
|
+
value = primitive_to_value(id)
|
22
|
+
|
23
|
+
unless slice_includes_record?(slice, resource)
|
24
|
+
slice[value[:v]] = {
|
25
|
+
_id: value
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
value[:v]
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns a normalised fields array for a record from a slice.
|
33
|
+
def values_from_slice(slice, id, field, website_iri = nil)
|
34
|
+
values = field_from_slice(slice, id, field, website_iri)
|
35
|
+
|
36
|
+
normalise_slice_values(values)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns the fields for a record from a slice.
|
40
|
+
def field_from_slice(slice, id, field, website_iri = nil)
|
41
|
+
record = record_from_slice(slice, id, website_iri)
|
42
|
+
return unless record.present?
|
43
|
+
|
44
|
+
field_from_record(record, field)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns a record from a slice.
|
48
|
+
def record_from_slice(slice, id, website_iri = nil)
|
49
|
+
slice[absolutized_id(retrieve_id(id), website_iri)] || slice[retrieve_id(id)]
|
50
|
+
end
|
51
|
+
|
52
|
+
def field_from_record(record, field)
|
53
|
+
field = URI(field.to_s)
|
54
|
+
symbolized = (field.fragment || field.path.split("/").last).camelize(:lower)
|
55
|
+
(record[field.to_s] || record[symbolized])&.compact
|
56
|
+
end
|
57
|
+
|
58
|
+
def retrieve_id(resource)
|
59
|
+
return resource.to_s if resource.is_a?(URI) || resource.is_a?(RDF::URI)
|
60
|
+
|
61
|
+
resource.try(:iri) || resource.try(:subject) || resource.try(:id) || resource
|
62
|
+
end
|
63
|
+
|
64
|
+
def absolutized_id(id, website_iri = nil)
|
65
|
+
return id.to_s.delete_prefix(website_iri) if website_iri.present?
|
66
|
+
|
67
|
+
id.to_s
|
68
|
+
end
|
69
|
+
|
70
|
+
def normalise_slice_values(values)
|
71
|
+
values.is_a?(Array) ? values.map(&:with_indifferent_access) : values&.with_indifferent_access
|
72
|
+
end
|
73
|
+
|
74
|
+
def expand(slice, website_iri = current_tenant)
|
75
|
+
return slice unless website_iri.present?
|
76
|
+
|
77
|
+
slice.transform_keys { |k| k.start_with?("/") ? website_iri + k : k }
|
78
|
+
end
|
79
|
+
|
80
|
+
def field_to_symbol(uri)
|
81
|
+
(uri.fragment || uri.path.split("/").last).camelize(:lower)
|
82
|
+
end
|
83
|
+
|
84
|
+
def slice_includes_record?(slice, resource)
|
85
|
+
slice.key?(object_to_value(resource)[:v])
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Empathy
|
4
|
+
module EmpJson
|
5
|
+
module Helpers
|
6
|
+
# Functions relating to adding values to records
|
7
|
+
module Values
|
8
|
+
def add_attribute_to_record(slice, rid, key, value, symbolize)
|
9
|
+
return if value.nil?
|
10
|
+
|
11
|
+
symbol = uri_to_symbol(key, symbolize)
|
12
|
+
emp_value = value_to_emp_value(value)
|
13
|
+
current_value = slice[rid][symbol]
|
14
|
+
|
15
|
+
slice[rid][symbol] =
|
16
|
+
if current_value.is_a?(Array)
|
17
|
+
[*current_value, *(emp_value.is_a?(Array) ? emp_value : [emp_value])]
|
18
|
+
elsif current_value
|
19
|
+
[current_value, *(emp_value.is_a?(Array) ? emp_value : [emp_value])]
|
20
|
+
else
|
21
|
+
emp_value
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def wrap_relation_in_sequence(value, resource)
|
26
|
+
LinkedRails::Sequence.new(
|
27
|
+
value.is_a?(Array) ? value : [value],
|
28
|
+
parent: resource,
|
29
|
+
scope: false
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
def uri_to_symbol(uri, symbolize)
|
34
|
+
return uri.to_s unless symbolize
|
35
|
+
|
36
|
+
casing = symbolize == :class ? :upper : :lower
|
37
|
+
|
38
|
+
(uri.fragment || uri.path.split("/").last).camelize(casing)
|
39
|
+
end
|
40
|
+
|
41
|
+
def value_to_emp_value(value)
|
42
|
+
return object_to_value(value) if value.nil?
|
43
|
+
|
44
|
+
case value
|
45
|
+
when SUPPORTS_SEQUENCE ? LinkedRails::Sequence : nil
|
46
|
+
object_to_value(value.iri)
|
47
|
+
when SUPPORTS_RDF_RB ? RDF::List : nil
|
48
|
+
object_to_value(value.subject)
|
49
|
+
when Array, *(SUPPORTS_AR ? [ActiveRecord::Associations::CollectionProxy, ActiveRecord::Relation] : [])
|
50
|
+
value.map { |v| object_to_value(v) }.compact
|
51
|
+
else
|
52
|
+
object_to_value(value)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Empathy
|
4
|
+
module EmpJson
|
5
|
+
module Serializer
|
6
|
+
# Functions relating to serializing attributes, relations and statements
|
7
|
+
module Fields
|
8
|
+
# Modifies the record parameter
|
9
|
+
def serialize_attributes_to_record(serializer, slice, resource, rid, serialization_params)
|
10
|
+
return [] if serializer.attributes_to_serialize.blank?
|
11
|
+
|
12
|
+
nested_resources = []
|
13
|
+
serializer.attributes_to_serialize.each do |_, attr|
|
14
|
+
next if attr.predicate.blank? || !attr.conditionally_allowed?(resource, serialization_params)
|
15
|
+
|
16
|
+
value = value_for_attribute(attr, resource, serialization_params)
|
17
|
+
symbol = predicate_to_symbol(attr)
|
18
|
+
add_attribute_to_record(slice, rid, symbol, value, false)
|
19
|
+
collect_nested_resources(nested_resources, slice, resource, value, serialization_params) if value.present?
|
20
|
+
end
|
21
|
+
|
22
|
+
nested_resources
|
23
|
+
end
|
24
|
+
|
25
|
+
# Modifies the slice parameter
|
26
|
+
def serialize_statements(serializer, slice, resource, serialization_params)
|
27
|
+
serializer._statements&.each do |key|
|
28
|
+
serializer.send(key, resource, serialization_params).each do |statement|
|
29
|
+
subject, predicate, value = unpack_statement(statement)
|
30
|
+
|
31
|
+
next if value.nil?
|
32
|
+
|
33
|
+
symbol = uri_to_symbol(predicate, symbolize)
|
34
|
+
rid = add_record_to_slice(slice, subject)
|
35
|
+
add_attribute_to_record(slice, rid, symbol, value, false)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Modifies the record parameter
|
41
|
+
def serialize_relations_to_record(serializer, slice, resource, rid, serialization_params)
|
42
|
+
return [] if serializer.relationships_to_serialize.blank?
|
43
|
+
|
44
|
+
nested_resources = []
|
45
|
+
serializer.relationships_to_serialize.each do |_, relationship|
|
46
|
+
next unless relationship.include_relationship?(resource, serialization_params)
|
47
|
+
|
48
|
+
value = value_for_relation(relationship, resource, serialization_params)
|
49
|
+
next if value.nil?
|
50
|
+
|
51
|
+
symbol = predicate_to_symbol(relationship)
|
52
|
+
add_attribute_to_record(slice, rid, symbol, value, false)
|
53
|
+
|
54
|
+
collect_nested_resources(nested_resources, slice, resource, value, serialization_params)
|
55
|
+
end
|
56
|
+
|
57
|
+
nested_resources
|
58
|
+
end
|
59
|
+
|
60
|
+
def value_for_attribute(attr, resource, serialization_params)
|
61
|
+
return resource.try(attr.method) if attr.method.is_a?(Symbol)
|
62
|
+
|
63
|
+
FastJsonapi.call_proc(attr.method, resource, serialization_params)
|
64
|
+
end
|
65
|
+
|
66
|
+
def value_for_relation(relation, resource, serialization_params)
|
67
|
+
value = unpack_relation_value(relation, resource, serialization_params)
|
68
|
+
|
69
|
+
return if value.nil?
|
70
|
+
|
71
|
+
if SUPPORTS_SEQUENCE && relation.sequence
|
72
|
+
wrap_relation_in_sequence(value, resource)
|
73
|
+
else
|
74
|
+
value
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def unpack_relation_value(relation, resource, serialization_params)
|
79
|
+
if relation.object_block
|
80
|
+
FastJsonapi.call_proc(relation.object_block, resource, serialization_params)
|
81
|
+
else
|
82
|
+
resource.try(relation.key)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def unpack_statement(statement)
|
87
|
+
subject = statement.try(:subject) || statement[0]
|
88
|
+
predicate = statement.try(:predicate) || statement[1]
|
89
|
+
value = statement.try(:object) || statement[2]
|
90
|
+
|
91
|
+
[subject, predicate, value]
|
92
|
+
end
|
93
|
+
|
94
|
+
def predicate_to_symbol(attr)
|
95
|
+
return uri_to_symbol(attr.predicate, symbolize) if attr.predicate.present?
|
96
|
+
|
97
|
+
attr.key
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Empathy
|
4
|
+
module EmpJson
|
5
|
+
module Serializer
|
6
|
+
# Functions relating to determining inclusion of records during serialization.
|
7
|
+
module Inclusion
|
8
|
+
# Modifies the nested_resources parameter
|
9
|
+
def collect_nested_resources(nested_resources, slice, resource, value, serialization_params)
|
10
|
+
case value
|
11
|
+
when LinkedRails::Sequence
|
12
|
+
collect_sequence_and_members(nested_resources, slice, resource, value, serialization_params)
|
13
|
+
when RDF::List
|
14
|
+
nested_resources.push value unless value.subject == NS.rdfv.nil
|
15
|
+
when Array
|
16
|
+
collect_array_members(nested_resources, slice, resource, value, serialization_params)
|
17
|
+
else
|
18
|
+
nested_resources.push value if blank_value?(value) || nested_resource?(resource, value)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def collect_sequence_and_members(nested_resources, slice, resource, value, serialization_params)
|
23
|
+
value.members.map { |m| collect_nested_resources(nested_resources, slice, resource, m, serialization_params) }
|
24
|
+
add_sequence_to_slice(slice, serialization_params, resource: value) unless slice[value.iri.to_s]
|
25
|
+
end
|
26
|
+
|
27
|
+
def collect_array_members(nested_resources, slice, resource, value, serialization_params)
|
28
|
+
value.each { |m| collect_nested_resources(nested_resources, slice, resource, m, serialization_params) }
|
29
|
+
end
|
30
|
+
|
31
|
+
def blank_value?(value)
|
32
|
+
value.try(:iri)&.is_a?(RDF::Node)
|
33
|
+
end
|
34
|
+
|
35
|
+
def nested_resource?(resource, value)
|
36
|
+
value.try(:iri)&.to_s&.include?("#") &&
|
37
|
+
!resource.iri.to_s.include?("#") &&
|
38
|
+
value.iri.to_s.starts_with?(resource.iri.to_s)
|
39
|
+
end
|
40
|
+
|
41
|
+
def process_includes(slice, serialization_params, **options)
|
42
|
+
includes = options.delete(:includes)
|
43
|
+
resource = options.delete(:resource)
|
44
|
+
|
45
|
+
includes.each do |prop, nested|
|
46
|
+
value = resource.try(prop)
|
47
|
+
next if value.blank?
|
48
|
+
|
49
|
+
(value.is_a?(Array) || value.is_a?(ActiveRecord::Relation) ? value : [value]).each do |v|
|
50
|
+
next if slice_includes_record?(slice, v)
|
51
|
+
|
52
|
+
resource_to_emp_json(slice, serialization_params, resource: v, includes: nested, **options)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support"
|
4
|
+
require "active_support/core_ext/object/try"
|
5
|
+
|
6
|
+
module Empathy
|
7
|
+
module EmpJson
|
8
|
+
module Serializer
|
9
|
+
# Functions relating to serializing records
|
10
|
+
module Records
|
11
|
+
def serialize_record(slice, serialization_params, **options)
|
12
|
+
resource = options.delete(:resource)
|
13
|
+
serializer = options.delete(:serializer_class) || RDF::Serializers.serializer_for(resource)
|
14
|
+
|
15
|
+
rid = add_record_to_slice(slice, resource)
|
16
|
+
|
17
|
+
nested = serialize_record_fields(serializer, slice, resource, rid, serialization_params)
|
18
|
+
|
19
|
+
nested
|
20
|
+
.reject { |r| slice_includes_record?(slice, r) }
|
21
|
+
.each { |r| resource_to_emp_json(slice, serialization_params, resource: r) }
|
22
|
+
process_includes(slice, serialization_params, resource: resource, **options) if options[:includes]
|
23
|
+
end
|
24
|
+
|
25
|
+
def serialize_record_fields(serializer, slice, resource, rid, serialization_params)
|
26
|
+
nested = serialize_attributes_to_record(serializer, slice, resource, rid, serialization_params)
|
27
|
+
nested += serialize_relations_to_record(serializer, slice, resource, rid, serialization_params)
|
28
|
+
|
29
|
+
serialize_statements(serializer, slice, resource, serialization_params)
|
30
|
+
|
31
|
+
nested
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Empathy
|
4
|
+
module EmpJson
|
5
|
+
module Serializer
|
6
|
+
# Functions relating to serializing RDF::Sequence records
|
7
|
+
module Sequence
|
8
|
+
def add_sequence_to_slice(slice, serialization_params, **options)
|
9
|
+
resource = options.delete(:resource)
|
10
|
+
serializer = options[:serializer_class] || RDF::Serializers.serializer_for(resource)
|
11
|
+
|
12
|
+
rid = add_record_to_slice(slice, resource)
|
13
|
+
|
14
|
+
serialize_attributes_to_record(serializer, slice, resource, rid, serialization_params)
|
15
|
+
process_sequence_members(serializer, slice, resource, rid, serialization_params, **options)
|
16
|
+
end
|
17
|
+
|
18
|
+
def process_sequence_members(serializer, slice, resource, rid, serialization_params, **options) # rubocop:disable Metrics/ParameterLists
|
19
|
+
nested = []
|
20
|
+
resource.members.each_with_index.map do |m, i|
|
21
|
+
process_sequence_member(m, i, nested, serializer, slice, resource, rid, serialization_params)
|
22
|
+
end
|
23
|
+
nested.each do |r|
|
24
|
+
resource_to_emp_json(slice, serialization_params, resource: r, includes: options[:includes])
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def process_sequence_member(member, index, nested, serializer, slice, resource, rid, serialization_params) # rubocop:disable Metrics/ParameterLists
|
29
|
+
index_predicate = serializer.relationships_to_serialize[:members].predicate
|
30
|
+
symbol = uri_to_symbol(index_predicate.call(self, index), symbolize)
|
31
|
+
slice[rid][symbol] = value_to_emp_value(member)
|
32
|
+
collect_nested_resources(nested, slice, resource, member, serialization_params)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Empathy
|
4
|
+
module EmpJson
|
5
|
+
module Serializer
|
6
|
+
# Module for serializing records to slices.
|
7
|
+
module Serializing
|
8
|
+
def render_emp_json(resource = @resource)
|
9
|
+
Oj.fast_generate(emp_json_hash(resource))
|
10
|
+
end
|
11
|
+
|
12
|
+
def emp_json_hash(_resource = @resource)
|
13
|
+
create_slice(@resource)
|
14
|
+
end
|
15
|
+
|
16
|
+
def create_slice(resources)
|
17
|
+
slice = {}
|
18
|
+
|
19
|
+
(resources.is_a?(Array) ? resources : [resources]).each do |resource|
|
20
|
+
resource_to_emp_json(
|
21
|
+
slice,
|
22
|
+
@params,
|
23
|
+
resource: resource,
|
24
|
+
serializer_class: self.class,
|
25
|
+
includes: @rdf_includes
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
slice
|
30
|
+
end
|
31
|
+
|
32
|
+
def resource_to_emp_json(slice, serialization_params, **options) # rubocop:disable Metrics/AbcSize
|
33
|
+
return if options[:resource].blank? || retrieve_id(options[:resource]).is_a?(Proc)
|
34
|
+
|
35
|
+
if SUPPORTS_SEQUENCE && options[:resource].is_a?(LinkedRails::Sequence)
|
36
|
+
add_sequence_to_slice(
|
37
|
+
slice,
|
38
|
+
serialization_params,
|
39
|
+
**options
|
40
|
+
)
|
41
|
+
elsif SUPPORTS_RDF_RB && options[:resource].is_a?(RDF::List)
|
42
|
+
add_rdf_list_to_slice(slice, symbolize, **options)
|
43
|
+
else
|
44
|
+
if options[:serializer_class] == RDF::Serializers::ListSerializer
|
45
|
+
raise("Trying to serialize mixed resources")
|
46
|
+
end
|
47
|
+
|
48
|
+
serialize_record(
|
49
|
+
slice,
|
50
|
+
serialization_params,
|
51
|
+
**options
|
52
|
+
)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "serializer/fields"
|
4
|
+
require_relative "serializer/inclusion"
|
5
|
+
require_relative "serializer/records"
|
6
|
+
require_relative "serializer/sequence"
|
7
|
+
require_relative "serializer/serializing"
|
8
|
+
|
9
|
+
module Empathy
|
10
|
+
module EmpJson
|
11
|
+
# Module to use with the rdf-serializers gem.
|
12
|
+
module Serializer
|
13
|
+
extend ActiveSupport::Concern
|
14
|
+
|
15
|
+
include Helpers::Primitives
|
16
|
+
include Helpers::RDFList
|
17
|
+
include Helpers::Slices
|
18
|
+
include Helpers::Values
|
19
|
+
include Fields
|
20
|
+
include Inclusion
|
21
|
+
include Records
|
22
|
+
include Sequence
|
23
|
+
include Serializing
|
24
|
+
|
25
|
+
included do
|
26
|
+
attr_accessor :symbolize
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(resource, opts = {})
|
30
|
+
self.symbolize = opts.delete(:symbolize)
|
31
|
+
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
35
|
+
def dump(*args, **options)
|
36
|
+
case args.first
|
37
|
+
when :empjson
|
38
|
+
render_emp_json
|
39
|
+
else
|
40
|
+
super(*args, **options)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "empathy/emp_json/helpers/primitives"
|
4
|
+
require "empathy/emp_json/helpers/rdf_list"
|
5
|
+
require "empathy/emp_json/helpers/slices"
|
6
|
+
require "empathy/emp_json/helpers/values"
|
7
|
+
require "empathy/emp_json/serializer"
|
8
|
+
|
9
|
+
module Empathy
|
10
|
+
module EmpJson
|
11
|
+
SUPPORTS_RDF_RB = Object.const_defined?("RDF")
|
12
|
+
SUPPORTS_SEQUENCE = Object.const_defined?("LinkedRails")
|
13
|
+
SUPPORTS_AR = Object.const_defined?("ActiveRecord")
|
14
|
+
|
15
|
+
class Error < StandardError; end
|
16
|
+
end
|
17
|
+
end
|
data/sig/emp/json/rb.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: emp_json
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Thom van Kalkeren
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-05-31 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Serializer for the EmpJson specification.
|
14
|
+
email:
|
15
|
+
- thom@ontola.io
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- ".rubocop.yml"
|
21
|
+
- CHANGELOG.md
|
22
|
+
- CODE_OF_CONDUCT.md
|
23
|
+
- Gemfile
|
24
|
+
- Gemfile.lock
|
25
|
+
- LICENSE.txt
|
26
|
+
- README.md
|
27
|
+
- Rakefile
|
28
|
+
- emp-json-rb.gemspec
|
29
|
+
- lib/empathy/emp_json.rb
|
30
|
+
- lib/empathy/emp_json/helpers/primitives.rb
|
31
|
+
- lib/empathy/emp_json/helpers/rdf_list.rb
|
32
|
+
- lib/empathy/emp_json/helpers/slices.rb
|
33
|
+
- lib/empathy/emp_json/helpers/values.rb
|
34
|
+
- lib/empathy/emp_json/serializer.rb
|
35
|
+
- lib/empathy/emp_json/serializer/fields.rb
|
36
|
+
- lib/empathy/emp_json/serializer/inclusion.rb
|
37
|
+
- lib/empathy/emp_json/serializer/records.rb
|
38
|
+
- lib/empathy/emp_json/serializer/sequence.rb
|
39
|
+
- lib/empathy/emp_json/serializer/serializing.rb
|
40
|
+
- lib/empathy/emp_json/version.rb
|
41
|
+
- sig/emp/json/rb.rbs
|
42
|
+
homepage: https://empathy.tools/tools/emp-json
|
43
|
+
licenses:
|
44
|
+
- MIT
|
45
|
+
metadata:
|
46
|
+
homepage_uri: https://empathy.tools/tools/emp-json
|
47
|
+
source_code_uri: https://github.com/empathy-tools/emp_json_rb
|
48
|
+
bug_tracker_uri: https://github.com/empathy-tools/emp_json_rb/issues
|
49
|
+
changelog_uri: https://github.com/empathy-tools/emp_json_rb/CHANGELOG.md
|
50
|
+
rubygems_mfa_required: 'true'
|
51
|
+
post_install_message:
|
52
|
+
rdoc_options: []
|
53
|
+
require_paths:
|
54
|
+
- lib
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: 2.7.0.preview1
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: '0'
|
65
|
+
requirements: []
|
66
|
+
rubygems_version: 3.3.7
|
67
|
+
signing_key:
|
68
|
+
specification_version: 4
|
69
|
+
summary: Serializer for the EmpJson specification.
|
70
|
+
test_files: []
|