Class: ClassFromSON

Inherits:
Object
  • Object
show all
Defined in:
lib/class_from_SON.rb

Overview

A utility to convert an input file of string-object notation, e.g. JSON, XML, YAML, and generate code that looks like a class of your desired language

Constant Summary collapse

@@target_languages =
[:java, :java_lombok, :ruby]
@@input_modes =
[:json]

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.error_and_exit(message) ⇒ Object



20
21
22
23
# File 'lib/class_from_SON.rb', line 20

def self.error_and_exit(message)
  puts "ERROR : #{message}"
  exit
end

.generate(dest_lang, source, source_lang, make_file = true, force_file = false) ⇒ Object

Will generate classes from a SON string Regardless of whether or not files are written, this will return an array of hashes; each hash represents a file, with two keys : :name for filename (without extension), and :contents for file contents

dest_lang is symbol file is filename & path source_lang is symbol make_file flag defaults to true; set to false if you do not want files to be created by this method force_file flag is false; set to true if you wish to overwrite matching destination files (use with caution!)



379
380
381
382
# File 'lib/class_from_SON.rb', line 379

def ClassFromSON.generate(dest_lang, source, source_lang, make_file = true, force_file = false)
  o = ClassFromSON.new
  o.generate(dest_lang, source, source_lang, make_file, force_file)
end

.generate_from_file(dest_lang, file, source_lang = nil, make_file = true, force_file = false) ⇒ Object

Will generate classes from a SON file Regardless of whether or not files are written, this will return an array of hashes; each hash represents a file, with two keys : :name for filename (without extension), and :contents for file contents

dest_lang is symbol file is filename & path source_lang is symbol or nil (if nil, source language will be determined from the file extension) make_file flag defaults to true; set to false if you do not want files to be created by this method force_file flag is false; set to true if you wish to overwrite matching destination files (use with caution!)



359
360
361
362
363
364
365
366
367
368
369
# File 'lib/class_from_SON.rb', line 359

def ClassFromSON.generate_from_file(dest_lang, file, source_lang = nil, make_file = true, force_file = false)

  error_and_exit "Could not locate file #{file}" unless File.exists?(file)

  source_lang ||= File.extname(file).gsub(".", "")
  source = File.readlines(file).join
  ClassFromSON.generate(dest_lang, source, source_lang, make_file, force_file)

  # o = ClassFromSON.new

  # o.generate(dest_lang, source, source_lang, make_file)

end

Instance Method Details

#convert_array_to_type(value_types) ⇒ Object

Translate “Array” into the desired output language Also needs the ‘value types’, i.e. what type is this array? A list of strings? A list of ints?



97
98
99
100
101
102
103
104
105
# File 'lib/class_from_SON.rb', line 97

def convert_array_to_type(value_types)
  error_and_exit "Detected an array, but could not determine the type of its children; found #{value_types.size} possibilities" unless value_types.size == 1
  case @language
  when :java, :java_lombok
    return "List<#{convert_ruby_type_to_type(value_types[0])}>"
  else 
    error_and_exit "Could not convert to output language #{@language}"
  end
end

#convert_boolean_to_typeObject

Translate “Fixnum” into the desired output language



76
77
78
79
80
81
82
83
# File 'lib/class_from_SON.rb', line 76

def convert_boolean_to_type
  case @language
  when :java, :java_lombok
    return "boolean"
  else 
    error_and_exit "Could not convert to output language #{@language}"
  end
end

#convert_custom_class_type(type) ⇒ Object

Returns code representing the start of the class



120
121
122
123
124
125
126
127
# File 'lib/class_from_SON.rb', line 120

def convert_custom_class_type(type)
  case @language
  when :java, :java_lombok, :ruby
    return type.capitalize_first_letter_only
  else 
    error_and_exit "Could not convert to output language #{@language}"
  end
end

#convert_fixnum_to_typeObject

Translate “Fixnum” into the desired output language



56
57
58
59
60
61
62
63
# File 'lib/class_from_SON.rb', line 56

def convert_fixnum_to_type
  case @language
  when :java, :java_lombok
    return "int"
  else 
    error_and_exit "Could not convert to output language #{@language}"
  end
end

#convert_float_to_typeObject

Translate “Fixnum” into the desired output language



66
67
68
69
70
71
72
73
# File 'lib/class_from_SON.rb', line 66

def convert_float_to_type
  case @language
  when :java, :java_lombok
    return "float"
  else 
    error_and_exit "Could not convert to output language #{@language}"
  end
end

#convert_hash_to_type(value_types) ⇒ Object

Translate “Hash” into the desired output language Also needs the ‘value types’, i.e. what type is this hash? A map of strings to booleans? A map of ints to strings?



109
110
111
112
113
114
115
116
117
# File 'lib/class_from_SON.rb', line 109

def convert_hash_to_type(value_types)
  error_and_exit "Detected a hash, but could not determine the type of its keys and values; found #{value_types.size} possibilities" unless value_types.size == 2
  case @language
  when :java, :java_lombok
    return "HashMap<#{convert_ruby_type_to_type(value_types[0])}, #{convert_ruby_type_to_type(value_types[1])}>"
  else 
    error_and_exit "Could not convert to output language #{@language}"
  end
end

#convert_ruby_type_to_type(type, value_types = []) ⇒ Object



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/class_from_SON.rb', line 29

def convert_ruby_type_to_type(type, value_types = [])
  # puts "#{__method__} called with type '#{type.inspect}', #{type.class}, #{value_types.inspect}"

  # Because type is an instance of Class, we need to compare names, so convert to String

  # Use .to_s instead of .name to cope with custom types (i.e. types that are themselves new classes to be generated)

  case type.to_s
  when "Fixnum", "Integer"
    converted = convert_fixnum_to_type
  when "Float"
    converted = convert_float_to_type
  when "String"
    converted = convert_string_to_type
  when "TrueClass", "FalseClass"
    converted = convert_boolean_to_type
  when "Array"
    converted = convert_array_to_type(value_types)
  when "Hash"
    converted = convert_hash_to_type(value_types)
  when "NilClass" # default nil to String

    converted = convert_string_to_type
  else
    converted = convert_custom_class_type(type)
  end
  # puts "Converted '#{type.inspect}' to #{converted}"

  converted
end

#convert_string_to_typeObject

Translate “String” into the desired output language



86
87
88
89
90
91
92
93
# File 'lib/class_from_SON.rb', line 86

def convert_string_to_type
  case @language
  when :java, :java_lombok
    return "String"
  else 
    error_and_exit "Could not convert to output language #{@language}"
  end
end

#error(message) ⇒ Object



11
12
13
# File 'lib/class_from_SON.rb', line 11

def error(message)
  puts "ERROR : #{message}"
end

#error_and_exit(message) ⇒ Object



15
16
17
18
# File 'lib/class_from_SON.rb', line 15

def error_and_exit(message)
  puts "ERROR : #{message}"
  exit
end

#generate(dest_lang, source, source_lang, make_file = true, force_file = false) ⇒ Object

Will generate classes from a SON string. Regardless of whether or not files are written, this will return an array of hashes; each hash represents a file, with two keys : :name for filename (without extension), and :contents for file contents

dest_lang is symbol file is filename & path source_lang is symbol make_file flag defaults to true; set to false if you do not want files to be created by this method force_file flag is false; set to true if you wish to overwrite matching destination files (use with caution!)



392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
# File 'lib/class_from_SON.rb', line 392

def generate(dest_lang, source, source_lang, make_file = true, force_file = false)

  error_and_exit "Please supply first argument as a Symbol" unless dest_lang.class == Symbol

  @language = dest_lang
  if @@target_languages.include?(@language)
    # may proceed

    # TODO other target languages, e.g. C#, Python

  else
    error_and_exit "Cannot generate language #{@language}; can only generate #{@@target_languages.join(", ")}"
  end
  @extension = set_file_extension_for_language

  @mode = source_lang.to_sym
  if @@input_modes.include?(@mode)
    # may proceed

  else
    error_and_exit "Cannot parse input language #{@mode}; can only parse #{@@input_modes.join(", ")}"
  end

  # TODO other input languages, e.g. XML, YAML

  case @mode
  when :json
    begin
      hash = JSON.parse(source)
    rescue JSON::ParserError => e
      error_and_exit "Could not parse supplied string as JSON. Error message : #{e.message}"
    end
  else
    error_and_exit "Cannot parse mode #{@mode}"
  end

  # If we have read in an array instead of a hash, then take the first element of the array

  # This assumes that each element in this top-level array has the same structure, which is reasonable

  if hash.class == Array && hash.size > 0
    hash = hash.shift
  end

  error_and_exit "Input file did not have a hash / map of key-value pairs; could not parse" unless hash.class == Hash

  error_and_exit "Input file hash / map was empty" if hash.empty?

  top_level_classname = generate_top_level_name
  output_classes = generate_output_classes(hash, top_level_classname).flatten # returns an array


  if make_file
    output_classes.each do |out|
      name = out[:name_with_ext]
      contents = out[:contents]
      unless force_file
        error_and_exit "Want to generate output file #{name}, but that file already exists" if File.exists?(name)
      end
      File.open(name, "w+") do |f|
        f.puts contents
      end
      puts "Wrote out file #{name}"
    end

    puts "Please inspect generated code files and adjust names and types accordingly"
  end
  output_classes
end

#generate_class_endObject

Returns code representing the end of the class



210
211
212
213
214
215
216
217
218
219
220
# File 'lib/class_from_SON.rb', line 210

def generate_class_end
  case @language
  when :java, :java_lombok
    class_end = "}"
  when :ruby 
    class_end = "end"
  else 
    error_and_exit "Could not convert to output language #{@language}"
  end
  class_end
end

#generate_class_start(name) ⇒ Object

Returns code representing the start of the class



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/class_from_SON.rb', line 174

def generate_class_start(name)
  case @language
  when :java
    start = "\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\npublic class \#{convert_custom_class_type(name)} {\n"
  when :java_lombok
    start = "\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lombok.Getter;\nimport lombok.NoArgsConstructor;\nimport lombok.Setter;\n\n@JsonInclude(Include.NON_NULL)\n@NoArgsConstructor // Need @NoArgsConstructor for JSON deserialisation\n@AllArgsConstructor // Need @AllArgsConstructor for @Builder\n@Builder\n@Getter\n@Setter\npublic class \#{convert_custom_class_type(name)} {\n"
  when :ruby
    start = "class #{convert_custom_class_type(name)}"
  else 
    error_and_exit "Could not convert to output language #{@language}"
  end
  start
end

#generate_classname(name) ⇒ Object

Returns an appropriately-formatted classname for the given name



141
142
143
144
145
146
147
148
# File 'lib/class_from_SON.rb', line 141

def generate_classname(name)
  case @language
  when :java, :java_lombok, :ruby
    return name.capitalize_first_letter_only
  else 
    error_and_exit "Could not convert to output language #{@language}"
  end
end

#generate_code_from_attributes(attributes) ⇒ Object

Returns code representing each of the supplied attributes



248
249
250
251
252
253
254
255
256
257
# File 'lib/class_from_SON.rb', line 248

def generate_code_from_attributes(attributes)
  case @language
  when :java, :java_lombok
    return generate_java_code_from_attributes(attributes)
  when :ruby
    return generate_ruby_code_from_attributes(attributes)
  else 
    error_and_exit "Could not convert to output language #{@language}"
  end
end

#generate_filename(name) ⇒ Object

Returns an appropriately-formatted filename for the given name



151
152
153
154
155
156
157
158
159
160
# File 'lib/class_from_SON.rb', line 151

def generate_filename(name)
  case @language
  when :java, :java_lombok
    return name.capitalize_first_letter_only + @extension
  when :ruby
    return name.snakecase + @extension
  else 
    error_and_exit "Could not convert to output language #{@language}"
  end
end

#generate_getter_and_setter(type, name) ⇒ Object



222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/class_from_SON.rb', line 222

def generate_getter_and_setter(type, name)
  lines = []
  case @language
  when :java_lombok
    # do nothing - Lombok's raison d'etre is to avoid getters & setters

  when :java
    # This is safe even if the name is already in snakecase

    field_name_for_getter = name.snakecase.pascalcase

    name = name.camelcase if name.include? "_"
      
    lines << "\t"
    lines << "\tpublic #{type} get#{field_name_for_getter}() {"
    lines << "\t\treturn #{name};"
    lines << "\t}"
    lines << "\t"
    lines << "\tpublic void set#{field_name_for_getter}(#{type} #{name}) {"   
    lines << "\t\tthis.#{name} = #{name};"
    lines << "\t}"
  else 
    error_and_exit "Could not convert to output language #{@language}"
  end
  lines
end

#generate_java_code_from_attributes(attributes) ⇒ Object

Returns Java code representing each of the supplied attributes



260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/class_from_SON.rb', line 260

def generate_java_code_from_attributes(attributes)
  code = []
  # Instance variables

  attributes.each do |att|
    if att[:name].include? "_"
      snakecase_name = att[:name]
      camelcase_name = att[:name].camelcase
      code << "\t@JsonProperty(\"#{snakecase_name}\")"
      code << "\tprivate #{convert_ruby_type_to_type(att[:type], att[:value_types])} #{camelcase_name};"
      code << "" # add a new line so that fields are separated & easier to read

    else 
      code << "\tprivate #{convert_ruby_type_to_type(att[:type], att[:value_types])} #{att[:name]};"
    end
  end

  #TODO constructor


  # Getters & setters

  attributes.each do |att|
    code << generate_getter_and_setter(convert_ruby_type_to_type(att[:type], att[:value_types]), att[:name])
  end
  code
end

#generate_output_classes(hash, top_level_classname = nil) ⇒ Object

From the supplied hash, generates code representing a class in the desired language (as a string) Returns an array of hashes; each hash represents a file, with two keys : :name for filename (without extension), and :contents for file contents



308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
# File 'lib/class_from_SON.rb', line 308

def generate_output_classes(hash, top_level_classname = nil)
  classname = generate_classname(top_level_classname)
  filename = generate_filename(classname)
  files = []
  this_file = {:name => classname, :name_with_ext => filename}
  lines = []
  lines << generate_class_start(classname)
  attributes = [] # array of hashes; keys => :name, :type, :value_types # :type, :value_types ([]) are initially kept as Ruby class names


  hash.each_pair do |k, v|
    attribute = {:name => k}
    if v.class == Array
      attribute[:type] = Array
      if v[0].class == Hash
        new_files = generate_output_classes(v[0], k)
        attribute[:value_types] = [new_files[0][:name]]
        files += new_files
      else
        # Array only contains primitives, not objects       

        attribute[:value_types] = [v[0].class]
      end      
    elsif v.class == Hash
      new_files = generate_output_classes(v, k)
      attribute[:type] = new_files[0][:name]
      files += new_files
    else
      attribute[:type] = v.class
    end
    attributes << attribute
  end

  lines << generate_code_from_attributes(attributes)
  lines << generate_class_end
  lines.flatten!
  this_file[:contents] = lines.join("\n")
  files.insert(0, this_file)
  files
end

#generate_ruby_code_from_attributes(attributes) ⇒ Object

Returns Ruby code representing each of the supplied attributes



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/class_from_SON.rb', line 285

def generate_ruby_code_from_attributes(attributes)
  code = []
  names = []
  attributes.each {|att| names << att[:name]}

  # Instance variables

  names.each do |name|
    code << "\tattr_accessor #{name.to_sym.inspect}"
  end
  code << "" # An empty string is enough to trigger a newline


  # Constructor

  code << "\tdef initialize(#{names.join(", ")})"
  names.each do |name|
    code << "\t\t@#{name} = #{name}"
  end
  code << "\tend"

  code
end

#generate_top_level_nameObject



129
130
131
132
133
134
135
136
137
138
# File 'lib/class_from_SON.rb', line 129

def generate_top_level_name
  case @language
  when :java, :java_lombok
    return "generatedFrom#{@mode.capitalize}"
  when :ruby
    return "generated_from_#{@mode}"
  else 
    error_and_exit "Could not convert to output language #{@language}"
  end
end

#set_file_extension_for_languageObject



162
163
164
165
166
167
168
169
170
171
# File 'lib/class_from_SON.rb', line 162

def set_file_extension_for_language
  case @language
  when :java, :java_lombok
    @extension = ".java"
  when :ruby
    @extension = ".rb"
  else 
    error_and_exit "Could not convert to output language #{@language}"
  end
end

#warn(message) ⇒ Object



25
26
27
# File 'lib/class_from_SON.rb', line 25

def warn(message)
  puts "WARN : #{message}"
end