rusty_json_schema 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e89b25e8e45ded120281d3b9b6af8a85bf5b8d0cb4f86b2f339485d629cf4dfd
4
- data.tar.gz: 1c6859709cd625af4184235dc09b1dfdaf05b8d0aca6fe8998c13c8bc11c1c4b
3
+ metadata.gz: dd6b9fde168fe809a7285e76360ae1a87c55c3e0603d4e4901b76f1b4c101674
4
+ data.tar.gz: '08b4831a5dff32132d473d17470fc992c4b6a453472366eca4f7b511b84fea2a'
5
5
  SHA512:
6
- metadata.gz: 27da18c9402b2f91c92a75f97f31ad0eb9761aa322634e79ac4dd4b735bdb1d9e138821991a41c03648be01cc8ec1f15a13868cf96f65421b1d6a00f2bac226f
7
- data.tar.gz: c09eaf66fce3eb9daa15c9ab78a23fa82276a7aeb2176ca23579936ff6bf798e7990731937662fc63c0d42ce7a6c59c45efbc4a5509722c84b3be16d1b3a481b
6
+ metadata.gz: 7b45b35d8d14862cf402215993c2075885ad014a2c80a41394bf9959495a4ecb5993d17bb31d479260f83746325e6c1b3527dfaa630af8967a884f4413ef9f5f
7
+ data.tar.gz: a70e61c95d6f55d66da0848fa4edc0a78dad1fa08da140eca8c2f0af8d0365f51eff7f8ee7dfdb855ffe6de1488b0c1499b0bddf3a357a9e34e89a0c4b414404
data/Cargo.toml CHANGED
@@ -9,5 +9,6 @@ name = "json_schema"
9
9
  crate-type = ["cdylib"]
10
10
 
11
11
  [dependencies]
12
+ libc = "0.2.81"
12
13
  jsonschema = "0.4.3"
13
14
  serde_json = "1.0"
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # RustyJSONSchema
2
2
 
3
- FFI wrapper around [`jsonschema`](https://github.com/Stranger6667/jsonschema-rs) rust library. Props go to original project.
3
+ [![Gem Version](https://badge.fury.io/rb/rusty_json_schema.svg)](https://badge.fury.io/rb/rusty_json_schema)
4
+
5
+ FFI wrapper around [`jsonschema`](https://github.com/Stranger6667/jsonschema-rs) Rust library. Props go to original project.
4
6
 
5
7
  Currently during heavy development.
6
8
 
@@ -14,11 +16,15 @@ gem "rusty_json_schema"
14
16
 
15
17
  And then execute:
16
18
 
17
- $ bundle install
19
+ ```
20
+ $ bundle install
21
+ ```
18
22
 
19
23
  Or install it yourself as:
20
24
 
21
- $ gem install rusty_json_schema
25
+ ```
26
+ $ gem install rusty_json_schema
27
+ ```
22
28
 
23
29
  ## Usage
24
30
 
@@ -32,13 +38,28 @@ Validate events like
32
38
 
33
39
  ```ruby
34
40
  validator.valid?(event_json)
41
+ # => true/false
42
+ ```
43
+
44
+ To get validation errors
45
+
46
+ ```ruby
47
+ validator.validate(event_json)
48
+ # => ["invalid...", ...]
35
49
  ```
36
50
 
37
51
  ## Development
38
52
 
39
53
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
40
54
 
41
- 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).
55
+ To install this gem onto your local machine, run `bundle exec rake install`.
56
+
57
+ To release a new version:
58
+
59
+ - update version number in `version.rb` & `CHANGELOG.md`
60
+ - create GitHub release with tag being new version prefixed with `v`, i.e. for `VERSION="0.1.0"` it would be `v0.1.0`
61
+ - pull `*.gem` artifact from release build
62
+ - `gem push *.gem` in order to publish it in [rubygems.org](https://rubygems.org).
42
63
 
43
64
  ## Contributing
44
65
 
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -1,62 +1,49 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "ffi"
4
+ require "json"
4
5
 
5
- require_relative "rusty_json_schema/version"
6
+ require "rusty_json_schema/version"
7
+ require "rusty_json_schema/nodes_array"
8
+ require "rusty_json_schema/validator"
9
+ require "rusty_json_schema/binding"
6
10
 
7
11
  # JSON Schema validation
8
12
  #
9
13
  # ## Example
10
14
  #
11
- # validator = RustyJSONSchema.build(schema) # where schema is a json string of the schema
12
- # validator.valid?(event) # where event is a json string of schema
15
+ # validator = RustyJSONSchema.build(schema)
16
+ #
17
+ # validator.valid?(event)
13
18
  # # => true/false
14
19
  #
20
+ # validator.validate(event)
21
+ # # => [] / ["...error messages", ...]
22
+ #
15
23
  module RustyJSONSchema
16
24
 
17
- def self.build(schema)
18
- RustyJSONSchema::Validator::Binding.new(schema)
19
- end
20
-
21
25
  class Error < StandardError; end
22
26
 
23
- # Handles release of the pointer automatically
24
- # with Ruby GC. This way we can intialize validator
25
- # in Rust, and hold a reference in Ruby.
26
- #
27
- class Validator < FFI::AutoPointer
27
+ class << self
28
28
 
29
- # Custom GC flow for our validator, freeing
30
- # the object within Rust
31
- #
32
- def self.release(pointer)
33
- Binding.free(pointer)
29
+ attr_writer :processor
30
+
31
+ def processor
32
+ @processor ||= JSON
34
33
  end
35
34
 
36
- # Simple validation without actual error messages
37
- #
38
- def valid?(event)
39
- Binding.is_valid(self, event)
35
+ def dump(data)
36
+ case data
37
+ when String then data
38
+ else processor.dump(data)
39
+ end
40
40
  end
41
41
 
42
- # FFI container for our library.
42
+ # Helper method that returns new instance of pointer
43
+ # to Validator struct.
43
44
  #
44
- module Binding
45
-
46
- extend FFI::Library
47
-
48
- lib_name =
49
- case ::FFI::Platform::LIBSUFFIX
50
- when "so", "dylib" then "libjson_schema"
51
- when "dll" then "json_schema"
52
- end
53
-
54
- ffi_lib File.expand_path("ext/#{lib_name}.#{::FFI::Platform::LIBSUFFIX}", __dir__)
55
-
56
- attach_function :new, :validator_new, [:string], Validator
57
- attach_function :free, :validator_free, [Validator], :void
58
- attach_function :is_valid, :validator_is_valid, [Validator, :string], :bool
59
-
45
+ def build(schema)
46
+ RustyJSONSchema::Binding.new(dump(schema))
60
47
  end
61
48
 
62
49
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RustyJSONSchema
4
+
5
+ # Integration point between Rust jsonschema wrapper
6
+ # and RustyJSONSchema.
7
+ #
8
+ module Binding
9
+
10
+ extend FFI::Library
11
+
12
+ lib_name =
13
+ case ::FFI::Platform::LIBSUFFIX
14
+ when "so", "dylib" then "libjson_schema"
15
+ when "dll" then "json_schema"
16
+ end
17
+
18
+ ffi_lib File.expand_path("../ext/#{lib_name}.#{::FFI::Platform::LIBSUFFIX}", __dir__)
19
+
20
+ attach_function :new, :validator_new, [:string], Validator
21
+ attach_function :free, :validator_free, [Validator], :void
22
+ attach_function :free_array, :array_free, [NodesArray], :void
23
+ attach_function :is_valid, :validator_is_valid, [Validator, :string], :bool
24
+ attach_function :validate, :validator_validate, [Validator, :string], NodesArray.by_ref
25
+
26
+ end
27
+
28
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RustyJSONSchema
4
+
5
+ # Struct representing list of errors returned from
6
+ # our wrapper library. Use ManagedStruct in order to
7
+ # properly release nested strings which would otherwise
8
+ # leak and pollute the memory.
9
+ #
10
+ class NodesArray < FFI::ManagedStruct
11
+
12
+ layout :data, :pointer,
13
+ :len, :uint,
14
+ :cap, :uint
15
+
16
+ def to_a
17
+ self[:data].get_array_of_string(0, self[:len]).compact
18
+ end
19
+
20
+ def self.release(ptr)
21
+ Binding.free_array(ptr)
22
+ end
23
+
24
+ end
25
+
26
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RustyJSONSchema
4
+
5
+ # Handles release of the pointer automatically
6
+ # with Ruby GC. This way we can intialize validator
7
+ # in Rust, and hold a reference in Ruby.
8
+ #
9
+ class Validator < FFI::AutoPointer
10
+
11
+ # Custom GC flow for our validator, freeing
12
+ # the object within Rust
13
+ #
14
+ def self.release(pointer)
15
+ Binding.free(pointer)
16
+ end
17
+
18
+ # Simple validation without actual error messages
19
+ #
20
+ def valid?(event)
21
+ Binding.is_valid(self, RustyJSONSchema.dump(event))
22
+ end
23
+
24
+ def validate(event)
25
+ Binding.validate(self, RustyJSONSchema.dump(event)).to_a
26
+ end
27
+
28
+ end
29
+
30
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RustyJSONSchema
4
4
 
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
 
7
7
  end
@@ -29,4 +29,5 @@ Gem::Specification.new do |spec|
29
29
 
30
30
  # Uncomment to register a new dependency of your gem
31
31
  spec.add_dependency "ffi", "~> 1.14"
32
+ spec.add_dependency "json", ">= 1.0"
32
33
  end
data/src/lib.rs CHANGED
@@ -1,8 +1,10 @@
1
+ extern crate libc;
2
+
1
3
  use jsonschema::JSONSchema;
2
4
  use serde_json::Value;
3
5
 
4
- use std::ffi::CStr;
5
- use std::os::raw::c_char;
6
+ use std::ffi::{CStr, CString};
7
+ use std::os::raw::{c_char, c_uint};
6
8
 
7
9
  /*
8
10
  * Our wrapper struct for schema and schema value,
@@ -22,7 +24,8 @@ impl Validator {
22
24
  */
23
25
  fn new(schema: Value) -> Validator {
24
26
  let boxed_schema: &'static Value = Box::leak(Box::new(schema));
25
- let boxed_compile: &'static JSONSchema<'static> = Box::leak(Box::new(JSONSchema::compile(boxed_schema).unwrap()));
27
+ let boxed_compile: &'static JSONSchema<'static> =
28
+ Box::leak(Box::new(JSONSchema::compile(boxed_schema).unwrap()));
26
29
 
27
30
  Validator {
28
31
  schema: boxed_compile,
@@ -33,6 +36,18 @@ impl Validator {
33
36
  fn is_valid(&self, event: &Value) -> bool {
34
37
  self.schema.is_valid(event)
35
38
  }
39
+
40
+ fn validate(&self, event: &Value) -> Vec<String> {
41
+ let mut errors: Vec<String> = vec![];
42
+
43
+ if let Err(validation_errors) = self.schema.validate(event) {
44
+ for error in validation_errors {
45
+ errors.push(error.to_string());
46
+ }
47
+ }
48
+
49
+ errors
50
+ }
36
51
  }
37
52
 
38
53
  impl Drop for Validator {
@@ -48,6 +63,36 @@ impl Drop for Validator {
48
63
  }
49
64
  }
50
65
 
66
+ #[repr(C)]
67
+ pub struct Array {
68
+ data: *mut *mut c_char,
69
+ len: c_uint,
70
+ cap: c_uint,
71
+ }
72
+
73
+ impl Array {
74
+ fn from_vec(from: Vec<String>) -> Self {
75
+ let mut converted: Vec<*mut c_char> = from
76
+ .into_iter()
77
+ .map(|s| CString::new(s).unwrap().into_raw())
78
+ .collect();
79
+
80
+ converted.shrink_to_fit();
81
+
82
+ let len = converted.len();
83
+ let cap = converted.capacity();
84
+ let result = Array {
85
+ data: converted.as_mut_ptr(),
86
+ len: len as c_uint,
87
+ cap: cap as c_uint,
88
+ };
89
+
90
+ std::mem::forget(converted);
91
+
92
+ result
93
+ }
94
+ }
95
+
51
96
  fn to_string(ptr: *const c_char) -> &'static CStr {
52
97
  unsafe {
53
98
  assert!(!ptr.is_null());
@@ -57,7 +102,7 @@ fn to_string(ptr: *const c_char) -> &'static CStr {
57
102
 
58
103
  #[no_mangle]
59
104
  pub extern "C" fn validator_new(c_schema: *const c_char) -> *mut Validator {
60
- let raw_schema = to_string(c_schema);
105
+ let raw_schema = to_string(c_schema);
61
106
  let schema = serde_json::from_slice(raw_schema.to_bytes()).unwrap();
62
107
  let validator = Validator::new(schema);
63
108
 
@@ -65,6 +110,7 @@ pub extern "C" fn validator_new(c_schema: *const c_char) -> *mut Validator {
65
110
  }
66
111
 
67
112
  #[no_mangle]
113
+ #[allow(clippy::not_unsafe_ptr_arg_deref)]
68
114
  pub extern "C" fn validator_free(ptr: *mut Validator) {
69
115
  if ptr.is_null() {
70
116
  return;
@@ -76,6 +122,7 @@ pub extern "C" fn validator_free(ptr: *mut Validator) {
76
122
  }
77
123
 
78
124
  #[no_mangle]
125
+ #[allow(clippy::not_unsafe_ptr_arg_deref)]
79
126
  pub extern "C" fn validator_is_valid(ptr: *const Validator, event: *const c_char) -> bool {
80
127
  let validator = unsafe {
81
128
  assert!(!ptr.is_null());
@@ -88,31 +135,134 @@ pub extern "C" fn validator_is_valid(ptr: *const Validator, event: *const c_char
88
135
  validator.is_valid(&event)
89
136
  }
90
137
 
138
+ #[no_mangle]
139
+ #[allow(clippy::not_unsafe_ptr_arg_deref)]
140
+ pub extern "C" fn validator_validate(ptr: *const Validator, event: *const c_char) -> *mut Array {
141
+ let validator = unsafe {
142
+ assert!(!ptr.is_null());
143
+ &*ptr
144
+ };
145
+
146
+ let raw_event = to_string(event);
147
+ let event: Value = serde_json::from_slice(raw_event.to_bytes()).unwrap();
148
+ let errors = validator.validate(&event);
149
+ let result = Array::from_vec(errors);
150
+ let boxed = Box::new(result);
151
+
152
+ Box::into_raw(boxed)
153
+ }
154
+
155
+ #[no_mangle]
156
+ #[allow(clippy::not_unsafe_ptr_arg_deref)]
157
+ pub extern "C" fn array_free(ptr: *mut Array) {
158
+ if ptr.is_null() {
159
+ return;
160
+ }
161
+
162
+ unsafe {
163
+ let array = Box::from_raw(ptr);
164
+ let data = Vec::from_raw_parts(array.data, array.len as usize, array.cap as usize);
165
+
166
+ for string in data {
167
+ let _ = CString::from_raw(string);
168
+ }
169
+ }
170
+ }
171
+
91
172
  #[cfg(test)]
92
173
  mod tests {
93
174
  // Note this useful idiom: importing names from outer (for mod tests) scope.
94
175
  use super::*;
95
176
  use std::ffi::CString;
96
- use std::{thread, time};
97
177
 
98
178
  /*
99
179
  * Simple sanity check if everything works together
100
180
  */
101
181
  #[test]
102
182
  fn test_valid_event() {
103
- let schema = CString::new("{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"array\",\"items\":[{\"type\":\"number\",\"exclusiveMaximum\":10}]}").unwrap();
104
- let valid_event = CString::new("[9]").unwrap();
105
- let invalid_event = CString::new("[22]").unwrap();
183
+ let validator = validator_new(helper_c_schema().as_ptr());
184
+
185
+ assert!(validator_is_valid(validator, helper_c_valid().as_ptr()));
186
+
187
+ assert!(!validator_is_valid(validator, helper_c_invalid().as_ptr()));
188
+
189
+ validator_free(validator);
190
+ }
191
+
192
+ #[test]
193
+ fn test_validate_event_when_valid() {
194
+ let validator = validator_new(helper_c_schema().as_ptr());
195
+ let raw_result = validator_validate(validator, helper_c_valid().as_ptr());
196
+ let result = unsafe { helper_validate_result_as_vec(raw_result) };
197
+
198
+ let expectation: Vec<String> = vec![];
199
+
200
+ assert_eq!(result, expectation);
106
201
 
107
- let c_schema_ptr: *const c_char = schema.as_ptr();
108
- let c_valid_event_ptr: *const c_char = valid_event.as_ptr();
109
- let c_invalid_event_ptr: *const c_char = invalid_event.as_ptr();
202
+ validator_free(validator);
203
+ }
204
+
205
+ #[test]
206
+ fn test_validate_event_when_invalid() {
207
+ let validator = validator_new(helper_c_schema().as_ptr());
208
+ let raw_result = validator_validate(validator, helper_c_invalid().as_ptr());
209
+ let result = unsafe { helper_validate_result_as_vec(raw_result) };
110
210
 
111
- let validator = validator_new(c_schema_ptr);
211
+ let expectation: Vec<String> = vec![
212
+ String::from("\'\"rusty\"\' is not of type \'number\'"),
213
+ String::from("\'1\' is not of type \'string\'"),
214
+ String::from("\'baz\' is a required property"),
215
+ ];
112
216
 
113
- assert!(validator_is_valid(validator, c_valid_event_ptr));
114
- assert!(!validator_is_valid(validator, c_invalid_event_ptr));
217
+ assert_eq!(result, expectation);
115
218
 
116
219
  validator_free(validator);
117
220
  }
221
+
222
+ /*
223
+ * Test helpers
224
+ */
225
+ fn helper_c_schema() -> CString {
226
+ CString::new(
227
+ r#"{
228
+ "properties":{
229
+ "foo": {"type": "string"},
230
+ "bar": {"type": "number"},
231
+ "baz": {}
232
+ },
233
+ "required": ["baz"]
234
+ }"#,
235
+ )
236
+ .unwrap()
237
+ }
238
+
239
+ fn helper_c_valid() -> CString {
240
+ CString::new(
241
+ r#"{
242
+ "foo": "rusty",
243
+ "bar": 1,
244
+ "baz": "rusty"
245
+ }"#,
246
+ )
247
+ .unwrap()
248
+ }
249
+
250
+ fn helper_c_invalid() -> CString {
251
+ CString::new(
252
+ r#"{
253
+ "foo": 1,
254
+ "bar": "rusty"
255
+ }"#,
256
+ )
257
+ .unwrap()
258
+ }
259
+
260
+ unsafe fn helper_validate_result_as_vec(result: *mut Array) -> Vec<String> {
261
+ let raw = Box::from_raw(result);
262
+
263
+ Vec::from_raw_parts(raw.data, raw.len as usize, raw.cap as usize)
264
+ .into_iter()
265
+ .map(|x| CString::from_raw(x).into_string().unwrap())
266
+ .collect()
267
+ }
118
268
  }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rusty_json_schema
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leszek Zalewski
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-28 00:00:00.000000000 Z
11
+ date: 2020-12-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.14'
27
+ - !ruby/object:Gem::Dependency
28
+ name: json
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
27
41
  description: |2
28
42
  FFI wrapper around https://github.com/Stranger6667/jsonschema-rs Rust library.
29
43
 
@@ -45,6 +59,9 @@ files:
45
59
  - lib/ext/libjson_schema.dylib
46
60
  - lib/ext/libjson_schema.so
47
61
  - lib/rusty_json_schema.rb
62
+ - lib/rusty_json_schema/binding.rb
63
+ - lib/rusty_json_schema/nodes_array.rb
64
+ - lib/rusty_json_schema/validator.rb
48
65
  - lib/rusty_json_schema/version.rb
49
66
  - rusty_json_schema.gemspec
50
67
  - src/lib.rs