rusty_json_schema 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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