nidyx 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,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