nidyx 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,174 @@
1
+ require "set"
2
+
3
+ module Nidyx
4
+ class ObjCProperty
5
+ attr_reader :name, :attributes, :type, :type_name, :desc, :getter_override,
6
+ :protocols
7
+
8
+ class UnsupportedEnumTypeError < StandardError; end
9
+
10
+ # @param property [Property] generic property
11
+ def initialize(property)
12
+ @name = property.name
13
+ @optional = property.optional
14
+ @desc = property.description
15
+
16
+ @getter_override = process_getter_override(name)
17
+ @type = process_type(property)
18
+ @attributes = ATTRIBUTES[@type]
19
+ @type_name = lookup_type_name(@type, property.class_name)
20
+
21
+ @protocols = []
22
+ @protocols += property.collection_types if property.collection_types
23
+ @protocols << "Optional" if @optional
24
+ end
25
+
26
+ # @return [Boolean] true if the obj-c property type is an object
27
+ def is_obj?
28
+ OBJECTS.include?(self.type)
29
+ end
30
+
31
+ # @return [Boolean] true if the property has protocols
32
+ def has_protocols?
33
+ !@protocols.empty?
34
+ end
35
+
36
+ # @return [String] the property's protocols, comma separated
37
+ def protocols_string
38
+ @protocols.join(", ")
39
+ end
40
+
41
+ private
42
+
43
+ PRIMITIVE_ATTRIBUTES = "assign, nonatomic"
44
+ OBJECT_ATTRIBUTES = "strong, nonatomic"
45
+
46
+ ATTRIBUTES = {
47
+ :array => OBJECT_ATTRIBUTES,
48
+ :boolean => PRIMITIVE_ATTRIBUTES,
49
+ :integer => PRIMITIVE_ATTRIBUTES,
50
+ :unsigned => PRIMITIVE_ATTRIBUTES,
51
+ :number => PRIMITIVE_ATTRIBUTES,
52
+ :number_obj => OBJECT_ATTRIBUTES,
53
+ :string => OBJECT_ATTRIBUTES,
54
+ :object => OBJECT_ATTRIBUTES,
55
+ :id => OBJECT_ATTRIBUTES
56
+ }
57
+
58
+ # Objective-C types
59
+ # :object intentionally omitted
60
+ TYPES = {
61
+ :array => "NSArray",
62
+ :boolean => "BOOL",
63
+ :integer => "NSInteger",
64
+ :unsigned => "NSUInteger",
65
+ :number => "double",
66
+ :number_obj => "NSNumber",
67
+ :string => "NSString",
68
+ :id => "id"
69
+ }
70
+
71
+ # Hash and Array intentionally omitted
72
+ ENUM_TYPES = {
73
+ Fixnum => :integer,
74
+ String => :string,
75
+ NilClass => :null,
76
+ Float => :number,
77
+ TrueClass => :boolean,
78
+ FalseClass => :boolean
79
+ }
80
+
81
+ OBJECTS = Set.new [:array, :number_obj, :string, :object, :id]
82
+
83
+ SIMPLE_NUMBERS = Set.new [:unsigned, :integer, :number]
84
+
85
+ BOXABLE_NUMBERS = SIMPLE_NUMBERS + [:boolean]
86
+
87
+ FORBIDDEN_PROPERTY_PREFIXES = ["new", "copy"]
88
+
89
+ # @param type [Symbol] an obj-c property type
90
+ # @param class_name [String] an object's type name
91
+ # @return [String] the property's type name
92
+ def lookup_type_name(type, class_name)
93
+ type == :object ? class_name : TYPES[type]
94
+ end
95
+
96
+ # @param property [Property] generic property
97
+ # @return [Symbol] an obj-c property type
98
+ def process_type(property)
99
+ return process_enum_type(property) if property.enum
100
+
101
+ type = property.type
102
+ if type.is_a?(Set)
103
+ process_array_type(type, property)
104
+ else
105
+ process_simple_type(type, property)
106
+ end
107
+ end
108
+
109
+
110
+ # @param property [Property] generic property
111
+ # @return [Symbol] an obj-c property type
112
+ def process_enum_type(property)
113
+ enum = property.enum
114
+
115
+ # map enum to a set of types
116
+ types = enum.map { |a| a.class }
117
+ raise UnsupportedEnumTypeError unless (types & [ Array, Hash ]).empty?
118
+ types = Set.new(types.map { |t| ENUM_TYPES[t] })
119
+
120
+ process_array_type(types, property)
121
+ end
122
+
123
+ # @param type [Symbol] a property type symbol
124
+ # @param property [Property] generic property
125
+ # @return [Symbol] an obj-c property type
126
+ def process_simple_type(type, property)
127
+ case type
128
+ when :boolean, :number, :integer
129
+ return :number_obj if @optional
130
+ if type == :integer && property.minimum && property.minimum >= 0
131
+ return :unsigned
132
+ end
133
+ type
134
+
135
+ when :null
136
+ @optional = true
137
+ :id
138
+
139
+ when :object
140
+ property.has_properties? ? :object : :id
141
+
142
+ else
143
+ type
144
+ end
145
+ end
146
+
147
+ # @param type [Set] an array of property types
148
+ # @param property [Property] generic property
149
+ # @return [Symbol] an obj-c property type
150
+ def process_array_type(types, property)
151
+ # if the key is optional
152
+ @optional = true if types.delete?(:null)
153
+
154
+ # single optional type
155
+ return process_simple_type(types.to_a.shift, property) if types.size == 1
156
+
157
+ # conjoined number types
158
+ return :number if types.subset?(SIMPLE_NUMBERS) && !@optional
159
+ return :number_obj if types.subset?(BOXABLE_NUMBERS)
160
+
161
+ :id
162
+ end
163
+
164
+ # @param name [String] the property name
165
+ # @return [String, nil] the getter override string if necessary
166
+ def process_getter_override(name)
167
+ FORBIDDEN_PROPERTY_PREFIXES.each do |p|
168
+ return ", getter=get#{name.camelize}" if name.match(/^#{p}[_A-Z].*/)
169
+ end
170
+
171
+ nil
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,25 @@
1
+ module Nidyx
2
+ module Output
3
+ extend self
4
+
5
+ # @param models [Hash] a full hash of models to output
6
+ # @param dir [String] output directory, defaults to current directory
7
+ def write(models, dir)
8
+ path = dir || Dir.getwd
9
+ models.each { |model| write_file(model, path) }
10
+ end
11
+
12
+ private
13
+
14
+ # @param model [Hash] all of the files for a specific model, stored in
15
+ # @param path [String] output directory
16
+ # a hash by extension
17
+ def write_file(model, path)
18
+ model.files.each do |file|
19
+ File.open(File.join(path, file.file_name), "w") do |f|
20
+ f.puts file.render
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,29 @@
1
+ module Nidyx
2
+ module ParseConstants
3
+
4
+ ###
5
+ # Schema key definitions
6
+ ###
7
+ REF_KEY = "$ref"
8
+ ENUM_KEY = "enum"
9
+ TYPE_KEY = "type"
10
+ DESCRIPTION_KEY = "description"
11
+ REQUIRED_KEY = "required"
12
+ PROPERTIES_KEY = "properties"
13
+ NAME_OVERRIDE_KEY = "nameOverride"
14
+ ITEMS_KEY = "items"
15
+ MINIMUM_KEY = "minimum"
16
+
17
+ ###
18
+ # Internal schema key definitions
19
+ ###
20
+ DERIVED_NAME = "__derivedName"
21
+ COLLECTION_TYPES_KEY = "__collectionTypes"
22
+
23
+ ###
24
+ # Object types
25
+ ###
26
+ OBJECT_TYPE = "object"
27
+ ARRAY_TYPE = "array"
28
+ end
29
+ end
@@ -0,0 +1,188 @@
1
+ require "nidyx/common"
2
+ require "nidyx/parse_constants"
3
+ require "nidyx/property"
4
+ require "nidyx/pointer"
5
+ require "nidyx/model"
6
+
7
+ include Nidyx::Common
8
+ include Nidyx::ParseConstants
9
+
10
+ module Nidyx
11
+ module Parser
12
+ extend self
13
+
14
+ class UnsupportedSchemaError < StandardError; end
15
+
16
+ # @param model_prefix [String] the prefix for model names
17
+ # @param schema [Hash] JSON Schema
18
+ # @param options [Hash] global application options
19
+ # @return [Hash] a Hash of ModelData objects
20
+ def parse(model_prefix, schema, options)
21
+ # setup parser
22
+ @class_prefix = model_prefix
23
+ @options = options
24
+ @schema = schema
25
+ @models = {}
26
+
27
+ # run model generation
28
+ generate([], class_name(@class_prefix, nil))
29
+ @models
30
+ end
31
+
32
+ private
33
+
34
+ # Generates a Model and adds it to the models array.
35
+ # @param path [Array] the path to an object in the schema
36
+ # @param name [String] raw model name
37
+ def generate(path, name)
38
+ object = get_object(path)
39
+
40
+ type = object[TYPE_KEY]
41
+ if type == OBJECT_TYPE
42
+ generate_object(path, name)
43
+
44
+ elsif type == ARRAY_TYPE
45
+ generate_top_level_array(path)
46
+
47
+ elsif type.is_a?(Array)
48
+ if type.include?(OBJECT_TYPE)
49
+ raise UnsupportedSchemaError if type.include?(ARRAY_TYPE)
50
+ generate_object(path, name)
51
+
52
+ elsif type.include?(ARRAY_TYPE)
53
+ generate_top_leve_array(path)
54
+
55
+ else raise UnsupportedSchemaError; end
56
+ else raise UnsupportedSchemaError; end
57
+ end
58
+
59
+ def generate_object(path, name)
60
+ @models[name] = model = Nidyx::Model.new(name)
61
+ required_properties = get_object(path)[REQUIRED_KEY]
62
+ properties_path = path + [PROPERTIES_KEY]
63
+
64
+ get_object(properties_path).keys.each do |key|
65
+ optional = is_optional?(key, required_properties)
66
+ property_path = properties_path + [key]
67
+ model.properties << generate_property(key, property_path, model, optional)
68
+ end
69
+ end
70
+
71
+ def generate_top_level_array(path)
72
+ resolve_array_refs(get_object(path))
73
+ end
74
+
75
+ # @param key [String] the key of the property in the JSON Schema
76
+ # @param path [Array] the path to the aforementioned object in the schema
77
+ # @param model [Property] the model that owns the property to be generated
78
+ # @param optional [Boolean] true if the property can be empty or null
79
+ def generate_property(key, path, model, optional)
80
+ obj = resolve_reference(path)
81
+ class_name = obj[DERIVED_NAME]
82
+
83
+ if include_type?(obj, OBJECT_TYPE) && obj[PROPERTIES_KEY]
84
+ model.dependencies << class_name
85
+ elsif include_type?(obj, ARRAY_TYPE)
86
+ obj[COLLECTION_TYPES_KEY] = resolve_array_refs(obj)
87
+ model.dependencies += obj[COLLECTION_TYPES_KEY]
88
+ end
89
+
90
+ name = obj[NAME_OVERRIDE_KEY] || key
91
+ property = Nidyx::Property.new(name, class_name, optional, obj)
92
+ property.overriden_name = key if obj[NAME_OVERRIDE_KEY]
93
+ property
94
+ end
95
+
96
+ # Given a path, which could be at any part of a reference chain, resolve
97
+ # the immediate schema object. This means:
98
+ #
99
+ # - if there is an imediate ref, follow it
100
+ # - inherit any schema information from the parent reference chain
101
+ # (unimplemented)
102
+ #
103
+ # If we are at the end of a chain, do the following:
104
+ #
105
+ # - generate a model for this object if necessary
106
+ # - add `class_name` to the immediate object when appropriate
107
+ # - return the immediate object
108
+ #
109
+ # @param path [Array] the path to an object in the schema
110
+ # @param parent [Hash, nil] the merged attributes of the parent reference chain
111
+ # @return [Hash] a modified schema object with inherited attributes from
112
+ # it's parents.
113
+ def resolve_reference(path, parent = nil)
114
+ obj = get_object(path)
115
+ ref = obj[REF_KEY]
116
+
117
+ # TODO: merge parent and obj into obj (destructive)
118
+
119
+ # If we find an immediate reference, chase it and pass the immediate
120
+ # object as a parent.
121
+ return resolve_reference_string(ref) if ref
122
+
123
+ # If we are dealing with an object, encode it's class name into the
124
+ # schema and generate it's model if necessary.
125
+ if include_type?(obj, OBJECT_TYPE) && obj[PROPERTIES_KEY]
126
+ obj[DERIVED_NAME] = class_name_from_path(@class_prefix, path, @schema)
127
+ generate(path, obj[DERIVED_NAME]) unless @models.has_key?(obj[DERIVED_NAME])
128
+ end
129
+
130
+ obj
131
+ end
132
+
133
+ # Resolves any references burried in the `items` property of an array
134
+ # definition. Returns a list of collection types in the array.
135
+ # @param obj [Hash] the array property schema
136
+ # @return [Array] list of types in the array
137
+ def resolve_array_refs(obj)
138
+ items = obj[ITEMS_KEY]
139
+ types = []
140
+
141
+ case items
142
+ when Array
143
+ items.each do |i|
144
+ resolve_reference_string(i[REF_KEY])
145
+ types << class_name_from_ref(i[REF_KEY])
146
+ end
147
+ when Hash
148
+ resolve_reference_string(items[REF_KEY])
149
+ types << class_name_from_ref(items[REF_KEY])
150
+ end
151
+
152
+ types.compact
153
+ end
154
+
155
+ def class_name_from_ref(ref)
156
+ class_name_from_path(@class_prefix, Nidyx::Pointer.new(ref).path, @schema) if ref
157
+ end
158
+
159
+ # Resolves a reference as a plain JSON Pointer string.
160
+ # @param ref [String] reference in json pointer format
161
+ # @return [Hash] a modified schema object with inherited attributes from
162
+ # it's parents.
163
+ def resolve_reference_string(ref)
164
+ resolve_reference(Nidyx::Pointer.new(ref).path) if ref
165
+ end
166
+
167
+ # @param path [Array] the path to an object in the global schema
168
+ # @return [Hash] a object containing JSON schema
169
+ def get_object(path)
170
+ object_at_path(path, @schema)
171
+ end
172
+
173
+ # @param key [String] the id of a specific property
174
+ # @param required_keys [Array] an array of the required property keys
175
+ # @return true if the property is optional
176
+ def is_optional?(key, required_keys)
177
+ !(required_keys && required_keys.include?(key))
178
+ end
179
+
180
+ # @param type_obj [Array, String] the JSON Schema type
181
+ # @param type [String] a string type to test
182
+ # @param true if the string type is a valid type according type object
183
+ def include_type?(obj, type)
184
+ type_obj = obj[TYPE_KEY]
185
+ type_obj.is_a?(Array) ? type_obj.include?(type) : type_obj == type
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,17 @@
1
+ # JSON Pointer
2
+ module Nidyx
3
+ class Pointer
4
+ attr_reader :source, :path
5
+
6
+ def initialize(str)
7
+ match = /^(?<source>.*)#\/*(?<path>.*)$/.match(str)
8
+ @source = match[:source]
9
+ @path = match[:path].split("/")
10
+ end
11
+
12
+ def to_s
13
+ puts "source: #{source}"
14
+ puts "path: #{path}"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,52 @@
1
+ require "set"
2
+ require "nidyx/parse_constants"
3
+
4
+ include Nidyx::ParseConstants
5
+
6
+ module Nidyx
7
+ class Property
8
+ attr_accessor :overriden_name
9
+ attr_reader :name, :class_name, :optional, :type, :description, :enum,
10
+ :collection_types, :minimum
11
+
12
+ class UndefinedTypeError < StandardError; end
13
+ class NonArrayEnumError < StandardError; end
14
+ class EmptyEnumError < StandardError; end
15
+
16
+ def initialize(name, class_name, optional, obj)
17
+ @name = name.camelize(false)
18
+ @class_name = class_name
19
+ @optional = optional
20
+ @enum = obj[ENUM_KEY]
21
+ @type = process_type(obj[TYPE_KEY], @enum)
22
+ @description = obj[DESCRIPTION_KEY]
23
+ @properties = obj[PROPERTIES_KEY]
24
+ @collection_types = obj[COLLECTION_TYPES_KEY]
25
+ @minimum = obj[MINIMUM_KEY]
26
+ end
27
+
28
+ def has_properties?
29
+ !!@properties
30
+ end
31
+
32
+ private
33
+
34
+ def process_type(type, enum)
35
+ if enum
36
+ raise NonArrayEnumError unless @enum.is_a?(Array)
37
+ raise EmptyEnumError if @enum.empty?
38
+ return
39
+ end
40
+
41
+ case type
42
+ when Array
43
+ raise UndefinedTypeError if type.empty?
44
+ Set.new(type.map { |t| t.to_sym })
45
+ when String
46
+ type.to_sym
47
+ else
48
+ raise UndefinedTypeError
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,45 @@
1
+ require "json"
2
+ require "nidyx/parse_constants"
3
+
4
+ include Nidyx::ParseConstants
5
+
6
+ module Nidyx
7
+ module Reader
8
+ extend self
9
+
10
+ class EmptySchemaError < StandardError; end
11
+
12
+ # Reads JSON from a file
13
+ # @param path [String] path of the file to read
14
+ # @return [Hash] the parsed JSON
15
+ def read(path)
16
+ schema = nil
17
+
18
+ begin
19
+ # TODO: validate this is legitimate JSON Schema
20
+ schema = JSON.parse(IO.read(path))
21
+ raise EmptySchemaError if empty_schema?(schema)
22
+ rescue JSON::JSONError => e
23
+ puts "Encountered an error reading JSON from #{path}"
24
+ puts e.message
25
+ exit 1
26
+ rescue EmptySchemaError
27
+ puts "Schema read from #{path} is empty"
28
+ exit 1
29
+ rescue StandardError => e
30
+ puts e.message
31
+ exit 1
32
+ end
33
+
34
+ schema
35
+ end
36
+
37
+ # @param schema [Hash] an object containing JSON schema
38
+ # @return [Boolean] true if the schema is empty
39
+ def empty_schema?(schema)
40
+ props = schema[PROPERTIES_KEY]
41
+ items = schema[ITEMS_KEY]
42
+ (!props || props.empty?) && (!items || items.empty?)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,3 @@
1
+ module Nidyx
2
+ VERSION = "0.1.0"
3
+ end
data/lib/nidyx.rb ADDED
@@ -0,0 +1 @@
1
+ require "nidyx/core_ext/string.rb"
@@ -0,0 +1,40 @@
1
+ require "minitest/autorun"
2
+ require "nidyx"
3
+
4
+ class TestCoreExt < Minitest::Test
5
+ def test_camelize
6
+ assert_equal("CamelCaseString", "camelCaseString".camelize)
7
+ assert_equal("CamelCaseString", "camel_case_string".camelize)
8
+ assert_equal("CamelCaseString", "Camel_Case_String".camelize)
9
+ assert_equal("CamelCaseString", "camel_Case_STRING".camelize)
10
+ assert_equal("1IsTheLoneliestNumber", "1_is_the_loneliest_number".camelize)
11
+ assert_equal("CCString", "CCString".camelize)
12
+ end
13
+
14
+ def test_camelize_with_optional_param
15
+ assert_equal("CamelCaseString", "camelCaseString".camelize(true))
16
+ assert_equal("camelCaseString", "CamelCaseString".camelize(false))
17
+ assert_equal("camelCaseString", "camel_case_string".camelize(false))
18
+ assert_equal("camelCaseString", "camel_case_string".camelize(false))
19
+ assert_equal("camelCaseString", "Camel_Case_String".camelize(false))
20
+ assert_equal("camelCaseString", "camel_Case_STRING".camelize(false))
21
+ assert_equal("1IsTheLoneliestNumber", "1_is_the_loneliest_number".camelize(false))
22
+ end
23
+
24
+ def test_camelize_unchanged
25
+ assert_equal("CamelCaseString", "CamelCaseString".camelize)
26
+ assert_equal("CamelCaseString", "CamelCaseString".camelize(true))
27
+ assert_equal("camelCaseString", "camelCaseString".camelize(false))
28
+ end
29
+
30
+ def test_camelize_with_plain_numbers
31
+ assert_equal("123121151", "123121151".camelize)
32
+ assert_equal("123121151", "123121151".camelize(false))
33
+ end
34
+
35
+ def test_camelize_not_destructive
36
+ string = "camel_case_string"
37
+ assert_equal("CamelCaseString", string.camelize)
38
+ assert_equal("camel_case_string", string)
39
+ end
40
+ end
@@ -0,0 +1,26 @@
1
+ require "minitest/autorun"
2
+ require "nidyx/objc/model_base"
3
+
4
+ class TestModelBase < Minitest::Test
5
+
6
+ MOCK_OPTS = {
7
+ :author => "test_author",
8
+ :company => "test_company",
9
+ :project => "test_project"
10
+ }
11
+
12
+ def test_empty_options
13
+ model = Nidyx::ObjCModelBase.new("ModelName", {})
14
+ assert_equal("ModelName", model.name)
15
+ assert_equal(nil, model.author)
16
+ assert_equal(nil, model.owner)
17
+ assert_equal(nil, model.project)
18
+ end
19
+
20
+ def test_full_options
21
+ model = Nidyx::ObjCModelBase.new("ModelName", MOCK_OPTS)
22
+ assert_equal("test_author", model.author)
23
+ assert_equal("test_company", model.owner)
24
+ assert_equal("test_project", model.project)
25
+ end
26
+ end