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, lenient_mode = true, custom_file_path = nil) ⇒ 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!) lenient_mode flag is true; if the SON contains different objects with the same name (e.g. “data”) then these will be treated

as different objects with a _1, _2, etc. suffix. If this flag is false and these different objects are present, then errors will occur

custom_file_path is nil; set to an absoulte or relative path to have the new files be written to that location



458
459
460
461
# File 'lib/class_from_SON.rb', line 458

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

.generate_from_file(dest_lang, file, source_lang = nil, make_file = true, force_file = false, lenient_mode = true, custom_file_path = nil) ⇒ 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!) lenient_mode flag is true; if the SON contains different objects with the same name (e.g. “data”) then these will be treated

as different objects with a _1, _2, etc. suffix. If this flag is false and these different objects are present, then errors will occur

custom_file_path is nil; set to an absoulte or relative path to have the new files be written to that location



435
436
437
438
439
440
441
442
443
444
445
# File 'lib/class_from_SON.rb', line 435

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

  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, lenient_mode, custom_file_path)

  # 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, lenient_mode = true, custom_file_path = nil) ⇒ 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!) lenient_mode flag is true; if the SON contains different objects with the same name (e.g. “data”) then these will be treated

as different objects with a _1, _2, etc. suffix. If this flag is false and these different objects are present, then errors will occur

custom_file_path is nil; set to an absoulte or relative path to have the new files be written to that location



474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
# File 'lib/class_from_SON.rb', line 474

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

  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


  # Set the directory that the files will be written into

  if custom_file_path
    # This caters for both absolute & relative file paths

    file_path = File.absolute_path(custom_file_path)
  else
    file_path = Dir.getwd
  end

  # Track the names of the classes/files we have written so far

  written_file_names = []

  if make_file
    output_classes.each do |out|
      name = out[:name_with_ext]
      # Check the name against the files we have already written

      if written_file_names.include?(name)
        if lenient_mode
          # Let us increment the name, e.g. "data.rb" -> "data_1.rb", "data_2.rb", etc.

          increment = 1
          new_name = name.gsub(@extension, "_#{increment}#{@extension}")
          while written_file_names.include?(new_name)
            increment += 1
            new_name = name.gsub(@extension, "_#{increment}#{@extension}")
          end
          name = new_name
        else
          message = "Want to generate output file #{name}, but a file with that name has already been written by this process. Your SON structure contains 2+ different classes with the same name"
          error_and_exit(message) 
        end
      end

      filename = file_path + File::SEPARATOR + name
      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(filename, "w+") do |f|
        f.puts contents
      end
      written_file_names << name
      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



220
221
222
223
224
225
226
227
228
229
230
# File 'lib/class_from_SON.rb', line 220

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
208
209
210
211
212
213
214
215
216
217
# File 'lib/class_from_SON.rb', line 174

def generate_class_start(name)
  # TODO make this more readable

  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
    case @mode
    when :json
    start = "require 'json'\n\nclass \#{convert_custom_class_type(name)}\n"
    else 
      error_and_exit "Cannot parse mode #{@mode}"
    end
  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



258
259
260
261
262
263
264
265
266
267
# File 'lib/class_from_SON.rb', line 258

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_from_and_to_methods(classname, attributes) ⇒ Object

Returns code for a from_SON and to_SON method



318
319
320
321
322
323
324
325
326
327
# File 'lib/class_from_SON.rb', line 318

def generate_from_and_to_methods(classname, attributes)
  case @language
  when :java, :java_lombok
    return generate_java_from_and_to_methods(classname, attributes)
  when :ruby
    return generate_ruby_from_and_to_methods(classname, attributes)
  else 
    error_and_exit "Could not convert to output language #{@language}"
  end
end

#generate_getter_and_setter(type, name) ⇒ Object



232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/class_from_SON.rb', line 232

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



270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'lib/class_from_SON.rb', line 270

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_java_from_and_to_methods(classname, attributes) ⇒ Object

Returns Java code for a from_SON and to_SON method



330
331
332
333
334
# File 'lib/class_from_SON.rb', line 330

def generate_java_from_and_to_methods(classname, attributes)
  code = []
  # TODO

  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



380
381
382
383
384
385
386
387
388
389
390
391
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
# File 'lib/class_from_SON.rb', line 380

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_from_and_to_methods(classname, 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



295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/class_from_SON.rb', line 295

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

  # 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

  # This is deliberately commented out, in favour of self.from_hash

  code << "\t# Using self.from_hash(hash) is usually better, but this code is here in case you prefer this style of constructor"
  code << "\t# def initialize(#{names.join(", ")})"
  names.each do |name|
    code << "\t\t# @#{name} = #{name}"
  end
  code << "\t# end"
  code
end

#generate_ruby_from_and_to_methods(classname, attributes) ⇒ Object

Returns Ruby code for a from_SON and to_SON method



337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# File 'lib/class_from_SON.rb', line 337

def generate_ruby_from_and_to_methods(classname, attributes)
  code = []
  names = []
  attributes.each {|att| names << att[:name].snakecase}

  # from_hash method

  code << ""
  code << "\tdef self.from_hash(h)"
  code << "\t\to = self.new"
  names.each do |name|
    code << "\t\to.#{name} = h[:#{name}]"
  end
  code << "\t\to"
  code << "\tend"

  # from_SON method

  code << ""
  code << "\tdef self.from_#{@mode}(#{@mode})"
  case @mode
  when :json
    code << "\t\tself.from_hash(JSON.parse(#{@mode}))"
    # TODO other input languages, e.g. XML, YAML

  end
  code << "\tend"
  
  # to_SON method

  code << ""
  code << "\tdef to_#{@mode}"
  code << "\t\th = {}"
  names.each do |name|
    code << "\t\th[:#{name}] = @#{name}"
  end
  case @mode
  when :json
    code << "\t\tJSON.generate(h)"
    # TODO other input languages, e.g. XML, YAML

  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