argo 0.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: eec272ff117353e33f1c251074a13a9b80ee0133
4
+ data.tar.gz: 8849f7c278b5f83c27699d5e10a39bc3e32e68fa
5
+ SHA512:
6
+ metadata.gz: d936009d4cb7595e92127e2b0ed8d47ec391a53b780fd66f59f68e12144ed00e72eb79beb648112167b84c634767fc24fe7283975b510f06b6b7164afd2fd2e6
7
+ data.tar.gz: adf136d679c8b065c63cc6ad9f388f907db1905468c44dbb9112d39dd8bdd1de1e8d88f20e154c8a4193334aa0e034a780a0a750d39074303df828fa55c94154
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2015 Paul Battley
2
+
3
+ Permission to use, copy, modify, and/or distribute this software for any
4
+ purpose with or without fee is hereby granted, provided that the above
5
+ copyright notice and this permission notice appear in all copies.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
8
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
9
+ FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
10
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
11
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
12
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
13
+ PERFORMANCE OF THIS SOFTWARE.
@@ -0,0 +1,12 @@
1
+ require 'argo/property'
2
+
3
+ module Argo
4
+ class ArrayProperty < Property
5
+ def initialize(items:, **kwargs)
6
+ super(**kwargs)
7
+ @items = items
8
+ end
9
+
10
+ attr_reader :items
11
+ end
12
+ end
@@ -0,0 +1,6 @@
1
+ require 'argo/property'
2
+
3
+ module Argo
4
+ class BooleanProperty < Property
5
+ end
6
+ end
@@ -0,0 +1,53 @@
1
+ require 'forwardable'
2
+
3
+ module Argo
4
+ class ConstraintProcessor
5
+ extend Forwardable
6
+
7
+ NON_CONSTRAINT_PROPERTIES = %w[
8
+ description
9
+ items
10
+ type
11
+ ]
12
+
13
+ def initialize(dereferencer)
14
+ @dereferencer = dereferencer
15
+ end
16
+
17
+ def process(hash)
18
+ process_hash(
19
+ hash.reject { |k, _| NON_CONSTRAINT_PROPERTIES.include?(k) }
20
+ )
21
+ end
22
+
23
+ private
24
+
25
+ def_delegators :@dereferencer, :dereference, :reference?
26
+
27
+ def symbolize_key(k)
28
+ k.gsub(/[A-Z]/) { |m| "_#{m.downcase}" }.to_sym
29
+ end
30
+
31
+ def process_object(obj)
32
+ case obj
33
+ when Hash
34
+ process_hash(obj)
35
+ when Array
36
+ obj.map { |v| process_object(v) }.freeze
37
+ else
38
+ obj.freeze
39
+ end
40
+ end
41
+
42
+ def process_hash(hash)
43
+ if reference?(hash)
44
+ dereference(hash)
45
+ else
46
+ hash.
47
+ map { |k, v| [symbolize_key(k), process_object(v)] }.
48
+ to_h.
49
+ freeze
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,23 @@
1
+ require 'delegate'
2
+
3
+ module Argo
4
+ class DeferredObject < Delegator
5
+ instance_methods.each do |m|
6
+ unless m =~ /^__|^(respond_to\?|method_missing|object_id)$/
7
+ undef_method m
8
+ end
9
+ end
10
+
11
+ def initialize(&block)
12
+ @__delegator_block__ = block
13
+ @__mutex__ = Mutex.new
14
+ end
15
+
16
+ def __getobj__
17
+ @__mutex__.synchronize do
18
+ @__getobj__ ||= @__delegator_block__.call
19
+ end
20
+ @__getobj__
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,26 @@
1
+ require 'argo/deferred_object'
2
+
3
+ module Argo
4
+ class Dereferencer
5
+ def initialize(&block)
6
+ @block = block
7
+ end
8
+
9
+ def dereference(hash)
10
+ path = hash.fetch('$ref')
11
+ fragments = path.split(/\//)
12
+ unless fragments[0] == '#'
13
+ raise "Can't dereference non-root-anchored path '#{path}'"
14
+ end
15
+ DeferredObject.new {
16
+ fragments.drop(1).inject(@block.call) { |schema, fragment|
17
+ schema.schemas.fetch(fragment)
18
+ }
19
+ }
20
+ end
21
+
22
+ def reference?(hash)
23
+ hash.key?('$ref')
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,32 @@
1
+ module Argo
2
+ class ImmutableKeywordStruct
3
+ # Disable these metrics here as gnarly metaprogramming is inherently ugly
4
+ # and splitting it up won't help much.
5
+ #
6
+ # rubocop:disable Metrics/AbcSize
7
+ # rubocop:disable Metrics/MethodLength
8
+ def self.new(default_parameters, &block)
9
+ Class.new do
10
+ define_method :initialize do |**kwargs|
11
+ unknown_keys = kwargs.keys - __defaults__.keys
12
+ if unknown_keys.any?
13
+ raise ArgumentError, "unknown keyword: #{unknown_keys.first}"
14
+ end
15
+ __defaults__.each do |key, default|
16
+ instance_variable_set("@#{key}", kwargs.fetch(key, default).freeze)
17
+ end
18
+ end
19
+
20
+ attr_reader(*default_parameters.keys)
21
+
22
+ class_eval(&block) if block
23
+
24
+ private
25
+
26
+ define_method :__defaults__ do
27
+ default_parameters
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,6 @@
1
+ require 'argo/property'
2
+
3
+ module Argo
4
+ class IntegerProperty < Property
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ require 'argo/property'
2
+
3
+ module Argo
4
+ class NumberProperty < Property
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ require 'argo/property'
2
+
3
+ module Argo
4
+ class ObjectProperty < Property
5
+ end
6
+ end
@@ -0,0 +1,20 @@
1
+ require 'argo/dereferencer'
2
+ require 'argo/schema_factory'
3
+
4
+ module Argo
5
+ class Parser
6
+ def initialize(source)
7
+ @source = source
8
+ end
9
+
10
+ def root
11
+ @root ||= SchemaFactory.new(dereferencer).build(@source)
12
+ end
13
+
14
+ private
15
+
16
+ def dereferencer
17
+ @dereferencer ||= Dereferencer.new { root }
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,11 @@
1
+ require 'argo/immutable_keyword_struct'
2
+
3
+ module Argo
4
+ Property = ImmutableKeywordStruct.new(
5
+ constraints: {},
6
+ description: nil,
7
+ required: false
8
+ ) do
9
+ alias_method :required?, :required
10
+ end
11
+ end
@@ -0,0 +1,91 @@
1
+ require 'forwardable'
2
+
3
+ require 'argo/array_property'
4
+ require 'argo/boolean_property'
5
+ require 'argo/integer_property'
6
+ require 'argo/number_property'
7
+ require 'argo/object_property'
8
+ require 'argo/string_property'
9
+
10
+ require 'argo/constraint_processor'
11
+
12
+ module Argo
13
+ class PropertyFactory
14
+ extend Forwardable
15
+
16
+ TYPE_MAP = {
17
+ 'array' => ArrayProperty,
18
+ 'boolean' => BooleanProperty,
19
+ 'integer' => IntegerProperty,
20
+ 'number' => NumberProperty,
21
+ 'object' => ObjectProperty,
22
+ 'string' => StringProperty
23
+ }
24
+
25
+ def initialize(dereferencer, required_fields = [])
26
+ @dereferencer = dereferencer
27
+ @required_fields = required_fields
28
+ end
29
+
30
+ def_delegators :@dereferencer, :dereference, :reference?
31
+
32
+ def build(body, name: nil)
33
+ class_for_type(body).new(
34
+ constraints: constraints(body),
35
+ description: body['description'],
36
+ required: required?(name),
37
+ **additional_properties(body)
38
+ )
39
+ end
40
+
41
+ private
42
+
43
+ def class_for_type(body)
44
+ klass = explicit_class(body) || implicit_class(body)
45
+ raise "No type found in #{body.inspect}" unless klass
46
+ klass
47
+ end
48
+
49
+ def explicit_class(body)
50
+ return nil unless body.key?('type')
51
+ type = body.fetch('type')
52
+ raise "Unknown property type '#{type}'" unless TYPE_MAP.key?(type)
53
+ TYPE_MAP.fetch(type)
54
+ end
55
+
56
+ def implicit_class(body)
57
+ if body.key?('enum')
58
+ StringProperty
59
+ elsif (body.keys & %w[ oneOf anyOf ]).any?
60
+ ObjectProperty
61
+ end
62
+ end
63
+
64
+ def additional_properties(body)
65
+ if class_for_type(body) == ArrayProperty
66
+ additional_properties_for_array(body)
67
+ else
68
+ {}
69
+ end
70
+ end
71
+
72
+ def additional_properties_for_array(body)
73
+ items = [body.fetch('items')].flatten.first
74
+ if reference?(items)
75
+ value = dereference(items)
76
+ else
77
+ factory = PropertyFactory.new(@dereferencer)
78
+ value = factory.build(items)
79
+ end
80
+ { items: value }
81
+ end
82
+
83
+ def required?(name)
84
+ @required_fields.include?(name)
85
+ end
86
+
87
+ def constraints(hash)
88
+ ConstraintProcessor.new(@dereferencer).process(hash)
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,13 @@
1
+ require 'argo/immutable_keyword_struct'
2
+
3
+ module Argo
4
+ Schema = ImmutableKeywordStruct.new(
5
+ description: nil,
6
+ properties: [],
7
+ schemas: {},
8
+ spec: nil,
9
+ title: nil,
10
+ type: :object,
11
+ route: nil
12
+ )
13
+ end
@@ -0,0 +1,71 @@
1
+ require 'forwardable'
2
+ require 'argo/schema'
3
+ require 'argo/property_factory'
4
+
5
+ module Argo
6
+ class SchemaFactory
7
+ extend Forwardable
8
+
9
+ def initialize(dereferencer)
10
+ @dereferencer = dereferencer
11
+ end
12
+
13
+ def_delegators :@dereferencer, :dereference, :reference?
14
+
15
+ def build(subgraph, route = [])
16
+ Schema.new(
17
+ description: extract_one(subgraph, :description),
18
+ title: extract_one(subgraph, :title),
19
+ schemas: subschemas(subgraph, route),
20
+ type: type(subgraph),
21
+ properties: properties(subgraph),
22
+ spec: extract_one(subgraph, :$schema),
23
+ route: route
24
+ )
25
+ end
26
+
27
+ NON_SUBSCHEMA_PROPERTIES = %w[
28
+ $schema
29
+ additionalItems
30
+ additionalProperties
31
+ description
32
+ id
33
+ properties
34
+ required
35
+ title
36
+ type
37
+ ]
38
+
39
+ def element_kind(key)
40
+ NON_SUBSCHEMA_PROPERTIES.include?(key) ? key.to_sym : :subschema
41
+ end
42
+
43
+ def extract(subgraph, kind)
44
+ subgraph.select { |k, _| element_kind(k) == kind }
45
+ end
46
+
47
+ def extract_one(subgraph, kind, default: nil)
48
+ _, v = subgraph.find { |k, _| element_kind(k) == kind }
49
+ v || default
50
+ end
51
+
52
+ def subschemas(subgraph, route)
53
+ extract(subgraph, :subschema).
54
+ inject({}) { |h, (k, v)| h.merge(k => build(v, route + [k])) }
55
+ end
56
+
57
+ def type(subgraph)
58
+ extract_one(subgraph, :type, default: :object).to_sym
59
+ end
60
+
61
+ def properties(subgraph)
62
+ required_fields = extract_one(subgraph, :required, default: [])
63
+ factory = PropertyFactory.new(@dereferencer, required_fields)
64
+ extract_one(subgraph, :properties, default: {}).
65
+ map { |k, v|
66
+ [k, reference?(v) ? dereference(v) : factory.build(v, name: k)]
67
+ }.
68
+ to_h
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,6 @@
1
+ require 'argo/property'
2
+
3
+ module Argo
4
+ class StringProperty < Property
5
+ end
6
+ end
@@ -0,0 +1,3 @@
1
+ module Argo
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,37 @@
1
+ require 'argo/parser'
2
+ require 'json'
3
+
4
+ RSpec.describe 'array reference' do
5
+ subject {
6
+ json = read_fixture('array_reference.json')
7
+ Argo::Parser.new(JSON.parse(json)).root
8
+ }
9
+
10
+ describe 'property with reference' do
11
+ subject { super().properties.fetch('a') }
12
+
13
+ it { is_expected.to be_kind_of(Argo::ArrayProperty) }
14
+
15
+ describe 'items' do
16
+ subject { super().items }
17
+
18
+ it 'has one property' do
19
+ expect(subject.properties.keys).to eq(%w[ thing_id ])
20
+ end
21
+ end
22
+ end
23
+
24
+ describe 'property with array containing one reference' do
25
+ subject { super().properties.fetch('b') }
26
+
27
+ it { is_expected.to be_kind_of(Argo::ArrayProperty) }
28
+
29
+ describe 'items' do
30
+ subject { super().items }
31
+
32
+ it 'has one property' do
33
+ expect(subject.properties.keys).to eq(%w[ thing_id ])
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,61 @@
1
+ require 'argo/parser'
2
+ require 'json'
3
+
4
+ RSpec.describe 'Example schemata' do
5
+ # See http://json-schema.org/examples.html
6
+
7
+ subject {
8
+ json = read_fixture('basic_example.json')
9
+ Argo::Parser.new(JSON.parse(json)).root
10
+ }
11
+
12
+ it 'has a title' do
13
+ expect(subject.title).
14
+ to eq('Example Schema')
15
+ end
16
+
17
+ it 'is an object' do
18
+ expect(subject.type).
19
+ to eq(:object)
20
+ end
21
+
22
+ describe 'properties' do
23
+ subject { super().properties }
24
+
25
+ it 'has three items' do
26
+ expect(subject.length).to eq(3)
27
+ end
28
+
29
+ describe 'firstName' do
30
+ subject { super().fetch('firstName') }
31
+
32
+ it { is_expected.to be_kind_of(Argo::StringProperty) }
33
+
34
+ it 'has no description' do
35
+ expect(subject.description).to be_nil
36
+ end
37
+
38
+ it 'is required' do
39
+ expect(subject).to be_required
40
+ end
41
+ end
42
+
43
+ describe 'age' do
44
+ subject { super().fetch('age') }
45
+
46
+ it { is_expected.to be_kind_of(Argo::IntegerProperty) }
47
+
48
+ it 'has a description' do
49
+ expect(subject.description).to eq('Age in years')
50
+ end
51
+
52
+ it 'is not required' do
53
+ expect(subject).not_to be_required
54
+ end
55
+
56
+ it 'has constraints' do
57
+ expect(subject.constraints).to eq(minimum: 0)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,48 @@
1
+ require 'argo/parser'
2
+ require 'json'
3
+
4
+ RSpec.describe 'Core spec section 3.4 nested schema' do
5
+ # See http://json-schema.org/latest/json-schema-core.html
6
+
7
+ subject {
8
+ json = read_fixture('core_nested_schema.json')
9
+ Argo::Parser.new(JSON.parse(json)).root
10
+ }
11
+
12
+ it 'has the title "root"' do
13
+ expect(subject.title).to eq('root')
14
+ end
15
+
16
+ it 'has an empty route' do
17
+ expect(subject.route).to eq([])
18
+ end
19
+
20
+ describe 'nested schema' do
21
+ subject { super().schemas.fetch('otherSchema') }
22
+
23
+ it 'has the title "nested"' do
24
+ expect(subject.title).to eq('nested')
25
+ end
26
+
27
+ it 'has a one-element route' do
28
+ expect(subject.route).to eq(%w[ otherSchema ])
29
+ end
30
+
31
+ describe 'doubly nested schema' do
32
+ subject { super().schemas.fetch('anotherSchema') }
33
+
34
+ it 'has the title "alsoNested"' do
35
+ expect(subject.title).to eq('alsoNested')
36
+ end
37
+
38
+ it 'has a two-element route' do
39
+ expect(subject.route).to eq(%w[ otherSchema anotherSchema ])
40
+ end
41
+ end
42
+ end
43
+
44
+ it 'is an object' do
45
+ expect(subject.type).
46
+ to eq(:object)
47
+ end
48
+ end
@@ -0,0 +1,19 @@
1
+ require 'argo/parser'
2
+ require 'json'
3
+
4
+ RSpec.describe 'Core spec section 3.4 simple schema' do
5
+ # See http://json-schema.org/latest/json-schema-core.html
6
+
7
+ subject {
8
+ json = read_fixture('core_simple_schema.json')
9
+ Argo::Parser.new(JSON.parse(json)).root
10
+ }
11
+
12
+ it 'has a title' do
13
+ expect(subject.title).to eq('root')
14
+ end
15
+
16
+ it 'is an object' do
17
+ expect(subject.type).to eq(:object)
18
+ end
19
+ end
@@ -0,0 +1,51 @@
1
+ require 'argo/parser'
2
+ require 'json'
3
+
4
+ RSpec.describe 'diskDevice' do
5
+ # See http://json-schema.org/example2.html
6
+
7
+ subject {
8
+ json = read_fixture('diskDevice.json')
9
+ Argo::Parser.new(JSON.parse(json)).root
10
+ }
11
+
12
+ it 'has no title' do
13
+ expect(subject.title).to be_nil
14
+ end
15
+
16
+ describe 'properties' do
17
+ subject { super().properties }
18
+
19
+ it 'has two items' do
20
+ expect(subject.length).to eq(2)
21
+ end
22
+
23
+ describe 'type' do
24
+ subject { super().fetch('type') }
25
+
26
+ it { is_expected.to be_kind_of(Argo::StringProperty) }
27
+
28
+ it 'is required' do
29
+ expect(subject).to be_required
30
+ end
31
+
32
+ it 'has an enum constraint' do
33
+ expect(subject.constraints).to eq(enum: %w[ disk ])
34
+ end
35
+ end
36
+
37
+ describe 'device' do
38
+ subject { super().fetch('device') }
39
+
40
+ it { is_expected.to be_kind_of(Argo::StringProperty) }
41
+
42
+ it 'is required' do
43
+ expect(subject).to be_required
44
+ end
45
+
46
+ it 'has a pattern constraint' do
47
+ expect(subject.constraints).to eq(pattern: '^/dev/[^/]+(/[^/]+)*$')
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,53 @@
1
+ require 'argo/parser'
2
+ require 'json'
3
+
4
+ RSpec.describe 'diskUUID' do
5
+ # See http://json-schema.org/example2.html
6
+
7
+ subject {
8
+ json = read_fixture('diskUUID.json')
9
+ Argo::Parser.new(JSON.parse(json)).root
10
+ }
11
+
12
+ it 'has no title' do
13
+ expect(subject.title).to be_nil
14
+ end
15
+
16
+ describe 'properties' do
17
+ subject { super().properties }
18
+
19
+ it 'has two items' do
20
+ expect(subject.length).to eq(2)
21
+ end
22
+
23
+ describe 'type' do
24
+ subject { super().fetch('type') }
25
+
26
+ it { is_expected.to be_kind_of(Argo::StringProperty) }
27
+
28
+ it 'is required' do
29
+ expect(subject).to be_required
30
+ end
31
+
32
+ it 'has an enum constraint' do
33
+ expect(subject.constraints).to eq(enum: %w[ disk ])
34
+ end
35
+ end
36
+
37
+ describe 'label' do
38
+ subject { super().fetch('label') }
39
+
40
+ it { is_expected.to be_kind_of(Argo::StringProperty) }
41
+
42
+ it 'is required' do
43
+ expect(subject).to be_required
44
+ end
45
+
46
+ it 'has a pattern constraint' do
47
+ expect(subject.constraints).to eq(
48
+ pattern: '^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$'
49
+ )
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,115 @@
1
+ require 'argo/parser'
2
+ require 'json'
3
+
4
+ RSpec.describe 'entry-schema' do
5
+ # See http://json-schema.org/example2.html
6
+
7
+ subject {
8
+ json = read_fixture('entry-schema.json')
9
+ Argo::Parser.new(JSON.parse(json)).root
10
+ }
11
+
12
+ it 'has a description' do
13
+ expect(subject.description).
14
+ to eq('schema for an fstab entry')
15
+ end
16
+
17
+ describe 'properties' do
18
+ subject { super().properties }
19
+
20
+ it 'has four items' do
21
+ expect(subject.length).to eq(4)
22
+ end
23
+
24
+ describe 'storage' do
25
+ subject { super().fetch('storage') }
26
+
27
+ it { is_expected.to be_kind_of(Argo::ObjectProperty) }
28
+
29
+ it 'is required' do
30
+ expect(subject).to be_required
31
+ end
32
+
33
+ describe 'one_of constraint' do
34
+ subject { super().constraints.fetch(:one_of) }
35
+
36
+ it 'has four entries' do
37
+ expect(subject.length).to eq(4)
38
+ end
39
+
40
+ describe 'first' do
41
+ subject { super().fetch(0) }
42
+
43
+ it { is_expected.to be_kind_of(Argo::Schema) }
44
+
45
+ describe 'properties' do
46
+ subject { super().properties }
47
+
48
+ it 'has two items' do
49
+ expect(subject.length).to eq(2)
50
+ end
51
+
52
+ describe 'type' do
53
+ subject { super().fetch('type') }
54
+
55
+ it { is_expected.to be_kind_of(Argo::StringProperty) }
56
+ end
57
+
58
+ describe 'device' do
59
+ subject { super().fetch('device') }
60
+
61
+ it { is_expected.to be_kind_of(Argo::StringProperty) }
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ describe 'fstype' do
69
+ subject { super().fetch('fstype') }
70
+
71
+ it { is_expected.to be_kind_of(Argo::StringProperty) }
72
+
73
+ it 'is not required' do
74
+ expect(subject).not_to be_required
75
+ end
76
+
77
+ it 'has an enum constraint' do
78
+ expect(subject.constraints).to eq(enum: %w[ ext3 ext4 btrfs ])
79
+ end
80
+ end
81
+
82
+ describe 'options' do
83
+ subject { super().fetch('options') }
84
+
85
+ it { is_expected.to be_kind_of(Argo::ArrayProperty) }
86
+
87
+ describe 'items' do
88
+ subject { super().items }
89
+
90
+ it { is_expected.to be_kind_of(Argo::StringProperty) }
91
+ end
92
+
93
+ it 'is not required' do
94
+ expect(subject).not_to be_required
95
+ end
96
+
97
+ it 'has minimum items and uniqueness constraints' do
98
+ expect(subject.constraints).to eq(
99
+ min_items: 1,
100
+ unique_items: true
101
+ )
102
+ end
103
+ end
104
+
105
+ describe 'readonly' do
106
+ subject { super().fetch('readonly') }
107
+
108
+ it { is_expected.to be_kind_of(Argo::BooleanProperty) }
109
+
110
+ it 'is not required' do
111
+ expect(subject).not_to be_required
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,71 @@
1
+ require 'argo/parser'
2
+ require 'json'
3
+
4
+ RSpec.describe 'nfs' do
5
+ # See http://json-schema.org/example2.html
6
+
7
+ subject {
8
+ json = read_fixture('nfs.json')
9
+ Argo::Parser.new(JSON.parse(json)).root
10
+ }
11
+
12
+ it 'has no title' do
13
+ expect(subject.title).to be_nil
14
+ end
15
+
16
+ describe 'properties' do
17
+ subject { super().properties }
18
+
19
+ it 'has three items' do
20
+ expect(subject.length).to eq(3)
21
+ end
22
+
23
+ describe 'type' do
24
+ subject { super().fetch('type') }
25
+
26
+ it { is_expected.to be_kind_of(Argo::StringProperty) }
27
+
28
+ it 'is required' do
29
+ expect(subject).to be_required
30
+ end
31
+
32
+ it 'has an enum constraint' do
33
+ expect(subject.constraints).to eq(enum: %w[ nfs ])
34
+ end
35
+ end
36
+
37
+ describe 'remotePath' do
38
+ subject { super().fetch('remotePath') }
39
+
40
+ it { is_expected.to be_kind_of(Argo::StringProperty) }
41
+
42
+ it 'is required' do
43
+ expect(subject).to be_required
44
+ end
45
+
46
+ it 'has a pattern constraint' do
47
+ expect(subject.constraints).to eq(pattern: '^(/[^/]+)+$')
48
+ end
49
+ end
50
+
51
+ describe 'server' do
52
+ subject { super().fetch('server') }
53
+
54
+ it { is_expected.to be_kind_of(Argo::StringProperty) }
55
+
56
+ it 'is required' do
57
+ expect(subject).to be_required
58
+ end
59
+
60
+ it 'has a one-of constraint' do
61
+ expect(subject.constraints).to eq(
62
+ one_of: [
63
+ { format: 'host-name' },
64
+ { format: 'ipv4' },
65
+ { format: 'ipv6' }
66
+ ]
67
+ )
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,27 @@
1
+ require 'argo/parser'
2
+ require 'json'
3
+
4
+ RSpec.describe 'property reference' do
5
+ subject {
6
+ json = read_fixture('property_reference.json')
7
+ Argo::Parser.new(JSON.parse(json)).root
8
+ }
9
+
10
+ describe 'a' do
11
+ subject { super().properties.fetch('a') }
12
+
13
+ it { is_expected.to be_kind_of(Argo::Schema) }
14
+ end
15
+
16
+ describe 'b' do
17
+ subject { super().properties.fetch('b') }
18
+
19
+ it { is_expected.to be_kind_of(Argo::ArrayProperty) }
20
+ end
21
+
22
+ describe 'c' do
23
+ subject { super().properties.fetch('c') }
24
+
25
+ it { is_expected.to be_kind_of(Argo::ArrayProperty) }
26
+ end
27
+ end
@@ -0,0 +1,114 @@
1
+ require 'argo/parser'
2
+ require 'json'
3
+
4
+ RSpec.describe 'Example schemata' do
5
+ subject {
6
+ json = read_fixture('simple_example.json')
7
+ Argo::Parser.new(JSON.parse(json)).root
8
+ }
9
+
10
+ it 'has a title' do
11
+ expect(subject.title).to eq('Product')
12
+ end
13
+
14
+ it 'is an object' do
15
+ expect(subject.type).to eq(:object)
16
+ end
17
+
18
+ it 'refers to the draft specification v4' do
19
+ expect(subject.spec).to eq('http://json-schema.org/draft-04/schema#')
20
+ end
21
+
22
+ it 'has a description' do
23
+ expect(subject.description).to eq("A product from Acme's catalog")
24
+ end
25
+
26
+ describe 'properties' do
27
+ subject { super().properties }
28
+
29
+ it 'has four items' do
30
+ expect(subject.length).to eq(4)
31
+ end
32
+
33
+ describe 'id' do
34
+ subject { super().fetch('id') }
35
+
36
+ it { is_expected.to be_kind_of(Argo::IntegerProperty) }
37
+
38
+ it 'has a description' do
39
+ expect(subject.description).
40
+ to eq('The unique identifier for a product')
41
+ end
42
+
43
+ it 'is required' do
44
+ expect(subject).to be_required
45
+ end
46
+
47
+ it 'has no constraints' do
48
+ expect(subject.constraints).to be_empty
49
+ end
50
+ end
51
+
52
+ describe 'name' do
53
+ subject { super().fetch('name') }
54
+
55
+ it { is_expected.to be_kind_of(Argo::StringProperty) }
56
+
57
+ it 'has a description' do
58
+ expect(subject.description).to eq('Name of the product')
59
+ end
60
+
61
+ it 'is required' do
62
+ expect(subject).to be_required
63
+ end
64
+
65
+ it 'has no constraints' do
66
+ expect(subject.constraints).to be_empty
67
+ end
68
+ end
69
+
70
+ describe 'price' do
71
+ subject { super().fetch('price') }
72
+
73
+ it { is_expected.to be_kind_of(Argo::NumberProperty) }
74
+
75
+ it 'has no description' do
76
+ expect(subject.description).to be_nil
77
+ end
78
+
79
+ it 'is required' do
80
+ expect(subject).to be_required
81
+ end
82
+
83
+ it 'has constraints' do
84
+ expect(subject.constraints).
85
+ to eq(minimum: 0, exclusive_minimum: true)
86
+ end
87
+ end
88
+
89
+ describe 'tags' do
90
+ subject { super().fetch('tags') }
91
+
92
+ it { is_expected.to be_kind_of(Argo::ArrayProperty) }
93
+
94
+ it 'has no description' do
95
+ expect(subject.description).to be_nil
96
+ end
97
+
98
+ it 'is not required' do
99
+ expect(subject).not_to be_required
100
+ end
101
+
102
+ it 'has constraints' do
103
+ expect(subject.constraints).
104
+ to eq(min_items: 1, unique_items: true)
105
+ end
106
+
107
+ describe 'items' do
108
+ subject { super().items }
109
+
110
+ it { is_expected.to be_kind_of(Argo::StringProperty) }
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,51 @@
1
+ require 'argo/parser'
2
+ require 'json'
3
+
4
+ RSpec.describe 'tmpfs' do
5
+ # See http://json-schema.org/example2.html
6
+
7
+ subject {
8
+ json = read_fixture('tmpfs.json')
9
+ Argo::Parser.new(JSON.parse(json)).root
10
+ }
11
+
12
+ it 'has no title' do
13
+ expect(subject.title).to be_nil
14
+ end
15
+
16
+ describe 'properties' do
17
+ subject { super().properties }
18
+
19
+ it 'has two items' do
20
+ expect(subject.length).to eq(2)
21
+ end
22
+
23
+ describe 'type' do
24
+ subject { super().fetch('type') }
25
+
26
+ it { is_expected.to be_kind_of(Argo::StringProperty) }
27
+
28
+ it 'is required' do
29
+ expect(subject).to be_required
30
+ end
31
+
32
+ it 'has constraints' do
33
+ expect(subject.constraints).to eq(enum: %w[ tmpfs ])
34
+ end
35
+ end
36
+
37
+ describe 'sizeInMB' do
38
+ subject { super().fetch('sizeInMB') }
39
+
40
+ it { is_expected.to be_kind_of(Argo::IntegerProperty) }
41
+
42
+ it 'is required' do
43
+ expect(subject).to be_required
44
+ end
45
+
46
+ it 'has constraints' do
47
+ expect(subject.constraints).to eq(minimum: 16, maximum: 512)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,25 @@
1
+ RSpec.configure do |config|
2
+ config.filter_run :focus
3
+ config.run_all_when_everything_filtered = true
4
+
5
+ if config.files_to_run.one?
6
+ config.default_formatter = 'doc'
7
+ end
8
+
9
+ config.order = :random
10
+
11
+ Kernel.srand config.seed
12
+
13
+ config.expect_with :rspec do |expectations|
14
+ expectations.syntax = :expect
15
+ end
16
+
17
+ config.mock_with :rspec do |mocks|
18
+ mocks.syntax = :expect
19
+ mocks.verify_partial_doubles = true
20
+ end
21
+
22
+ def read_fixture(name)
23
+ File.read(File.expand_path("../fixtures/#{name}", __FILE__))
24
+ end
25
+ end
@@ -0,0 +1,29 @@
1
+ require 'argo/deferred_object'
2
+
3
+ RSpec.describe Argo::DeferredObject do
4
+ subject { described_class.new { receiver.delegee } }
5
+ let(:receiver) { double(delegee: delegee) }
6
+ let(:delegee) { double(message: nil) }
7
+
8
+ it 'does not call the block at initialization time' do
9
+ expect(receiver).to receive(:delegee).never
10
+ subject
11
+ end
12
+
13
+ it 'calls the block when a method is called' do
14
+ expect(delegee).to receive(:message)
15
+ subject.message
16
+ end
17
+
18
+ it 'delegates the value to the receiver' do
19
+ allow(delegee).to receive(:message).and_return('VALUE')
20
+ expect(subject.message).to eq('VALUE')
21
+ end
22
+
23
+ it 'evaluates the block only once' do
24
+ expect(receiver).to receive(:delegee).once.and_return(delegee)
25
+ 2.times do
26
+ subject.message
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,54 @@
1
+ require 'argo/immutable_keyword_struct'
2
+
3
+ RSpec.describe Argo::ImmutableKeywordStruct do
4
+ let(:klass) {
5
+ described_class.new(a: nil, b: 'foo') do
6
+ def hello
7
+ "hello #{b}"
8
+ end
9
+ end
10
+ }
11
+
12
+ context 'with defaults' do
13
+ subject { klass.new }
14
+
15
+ it 'assigns properties with defaults' do
16
+ expect(subject.a).to be_nil
17
+ expect(subject.b).to eq('foo')
18
+ end
19
+
20
+ it 'freezes values' do
21
+ expect {
22
+ subject.b << ' oops'
23
+ }.to raise_exception(RuntimeError)
24
+ end
25
+
26
+ it 'allows method definition in a block' do
27
+ expect(subject.hello).to eq('hello foo')
28
+ end
29
+ end
30
+
31
+ context 'with assigned values' do
32
+ subject { klass.new(b: 'bar') }
33
+
34
+ it 'overrides defaults' do
35
+ expect(subject.b).to eq('bar')
36
+ end
37
+
38
+ it 'freezes values' do
39
+ expect {
40
+ subject.b << ' oops'
41
+ }.to raise_exception(RuntimeError)
42
+ end
43
+
44
+ it 'allows method definition in a block' do
45
+ expect(subject.hello).to eq('hello bar')
46
+ end
47
+ end
48
+
49
+ it 'raises an exception with an unrecognised keyword' do
50
+ expect {
51
+ klass.new(c: 1)
52
+ }.to raise_exception(ArgumentError)
53
+ end
54
+ end
metadata ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: argo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Paul Battley
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-06-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.9'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.9'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.32'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.32'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop-rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.3'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.3'
69
+ description: Expand JSON Schema(s) into introspectable Ruby objects
70
+ email: pbattley@gmail.com
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files:
74
+ - COPYING.txt
75
+ files:
76
+ - COPYING.txt
77
+ - lib/argo/array_property.rb
78
+ - lib/argo/boolean_property.rb
79
+ - lib/argo/constraint_processor.rb
80
+ - lib/argo/deferred_object.rb
81
+ - lib/argo/dereferencer.rb
82
+ - lib/argo/immutable_keyword_struct.rb
83
+ - lib/argo/integer_property.rb
84
+ - lib/argo/number_property.rb
85
+ - lib/argo/object_property.rb
86
+ - lib/argo/parser.rb
87
+ - lib/argo/property.rb
88
+ - lib/argo/property_factory.rb
89
+ - lib/argo/schema.rb
90
+ - lib/argo/schema_factory.rb
91
+ - lib/argo/string_property.rb
92
+ - lib/argo/version.rb
93
+ - spec/integration/array_reference_spec.rb
94
+ - spec/integration/basic_example_spec.rb
95
+ - spec/integration/core_nested_schema_spec.rb
96
+ - spec/integration/core_simple_schema_spec.rb
97
+ - spec/integration/disk_device_spec.rb
98
+ - spec/integration/disk_uuid_spec.rb
99
+ - spec/integration/entry_schema_spec.rb
100
+ - spec/integration/nfs_spec.rb
101
+ - spec/integration/property_reference_spec.rb
102
+ - spec/integration/simple_example_spec.rb
103
+ - spec/integration/tmpfs_spec.rb
104
+ - spec/spec_helper.rb
105
+ - spec/unit/deferred_object_spec.rb
106
+ - spec/unit/immutable_keyword_struct_spec.rb
107
+ homepage: https://github.com/threedaymonk/argo
108
+ licenses:
109
+ - ISC
110
+ metadata: {}
111
+ post_install_message:
112
+ rdoc_options: []
113
+ require_paths:
114
+ - lib
115
+ required_ruby_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: 2.1.0
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ requirements: []
126
+ rubyforge_project:
127
+ rubygems_version: 2.4.5
128
+ signing_key:
129
+ specification_version: 4
130
+ summary: JSON Schema expander
131
+ test_files: []