package org.jruby;
import org.jruby.anno.JRubyMethod;
import org.jruby.internal.runtime.methods.AttrReaderMethod;
import org.jruby.ir.operands.UndefinedValue;
import org.jruby.ir.runtime.IRRuntimeHelpers;
import org.jruby.runtime.Block;
import org.jruby.runtime.ClassIndex;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.Visibility;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.runtime.ivars.VariableAccessor;
import org.jruby.runtime.ivars.VariableTableManager;
import org.jruby.runtime.marshal.MarshalDumper;
import org.jruby.runtime.marshal.MarshalLoader;
import org.jruby.util.io.RubyInputStream;
import org.jruby.util.io.RubyOutputStream;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.stream.Collectors;
import static org.jruby.RubyBasicObject.rbInspect;
import static org.jruby.RubyHash.newSmallHash;
import static org.jruby.api.Convert.asFixnum;
import static org.jruby.api.Convert.toLong;
import static org.jruby.api.Convert.toSymbol;
import static org.jruby.api.Create.allocArray;
import static org.jruby.api.Create.newArray;
import static org.jruby.api.Create.newEmptyArray;
import static org.jruby.api.Create.newEmptyHash;
import static org.jruby.api.Create.newSmallHash;
import static org.jruby.api.Create.newString;
import static org.jruby.api.Define.defineClass;
import static org.jruby.api.Error.argumentError;
import static org.jruby.api.Error.keywordError;
import static org.jruby.api.Error.typeError;
import static org.jruby.ir.runtime.IRRuntimeHelpers.setCallInfo;
import static org.jruby.runtime.Arity.checkArgumentCount;
import static org.jruby.runtime.Helpers.invokedynamic;
import static org.jruby.runtime.ThreadContext.CALL_KEYWORD;
import static org.jruby.runtime.ThreadContext.hasNonemptyKeywords;
import static org.jruby.runtime.invokedynamic.MethodNames.HASH;
import static org.jruby.util.RubyStringBuilder.str;
public class RubyData {
private static final String MEMBERS_KEY = "__members__";
private static final String ACCESSORS_KEY = "__accessors__";
public static RubyClass createDataClass(ThreadContext context, RubyClass Object) {
RubyClass Data = defineClass(context, "Data", Object, ObjectAllocator.NOT_ALLOCATABLE_ALLOCATOR)
.classIndex(ClassIndex.DATA)
.defineMethods(context, RubyData.class);
Data.getSingletonClass().undefMethods(context, "new");
return Data;
}
@JRubyMethod(meta = true, rest = true)
public static RubyClass define(ThreadContext context, IRubyObject self, IRubyObject[] args, Block block) {
LinkedHashSet keySet = new LinkedHashSet();
for (int i = 0 ; i < args.length ; i++) {
RubySymbol mem = toSymbol(context, args[i]);
if (mem.validAttrsetName()) {
throw argumentError(context, "invalid data member: " + mem);
}
if (keySet.contains(mem)) {
throw argumentError(context, "duplicate member: " + mem);
}
keySet.add(mem);
}
RubyClass dataClass = newDataStruct(context, (RubyClass) self, keySet);
if (block.isGiven()) {
dataClass.module_eval(context, block);
}
return dataClass;
}
@JRubyMethod(keywords = true, rest = true)
public static void initialize(ThreadContext context, IRubyObject self, IRubyObject[] args) {
ThreadContext.resetCallInfo(context); // we don't directly use callInfo here
RubyBasicObject selfObj = (RubyBasicObject) self;
selfObj.checkFrozen();
RubyArray members = getStructMembers(self);
int numMembers = members.size();
if (args.length == 0) {
if (numMembers > 0) {
throw keywordError(context, "missing", members);
}
selfObj.setFrozen(true);
return;
}
if (args.length > 1 || !(args[0] instanceof RubyHash)) {
throw argumentError(context, args.length, 0, 0);
}
RubyHash hash = (RubyHash) args[0];
if (hash.size() < numMembers) {
RubyArray missing = (RubyArray) members.op_diff(context, hash.keys(context));
throw keywordError(context, "missing", missing);
}
RubyArray[] unknownKeywordsPtr = {null};
RubyClass metaClass = selfObj.getMetaClass();
VariableTableManager vtm = getVariableManagerFromClass(metaClass);
Map variableAccessors = vtm.getVariableAccessorsForRead();
hash.visitAll(context, (c, h, k, v, i) -> {
String keyString = toSymbol(context, k).idString();
VariableAccessor variableAccessor = variableAccessors.get(keyString);
if (variableAccessor != null) {
variableAccessor.set(self, v);
} else {
RubyArray unknownKeywords = unknownKeywordsPtr[0];
if (unknownKeywords == null) {
unknownKeywordsPtr[0] = unknownKeywords = newEmptyArray(context);
}
unknownKeywords.append(context, k);
}
});
selfObj.setFrozen(true);
if (unknownKeywordsPtr[0] != null) {
throw keywordError(context, "unknown", unknownKeywordsPtr[0]);
}
}
@JRubyMethod(name = "initialize_copy")
public static IRubyObject initialize_copy(ThreadContext context, IRubyObject copy, IRubyObject original) {
if (original != copy) {
original.getMetaClass().getVariableTableManager().syncVariables((RubyBasicObject) copy, original);
}
copy.setFrozen(true);
return copy;
}
@JRubyMethod(name = "==")
public static IRubyObject op_equal(ThreadContext context, IRubyObject self, IRubyObject other) {
return checkDataEquality(context, (RubyBasicObject) self, (RubyBasicObject) other, "==", RubyData::equalData);
}
@JRubyMethod(name = "eql?")
public static IRubyObject eql(ThreadContext context, IRubyObject self, IRubyObject other) {
return checkDataEquality(context, (RubyBasicObject) self, (RubyBasicObject) other, "eql?", RubyData::eqlData);
}
@JRubyMethod
public static IRubyObject hash(ThreadContext context, IRubyObject self) {
RubyBasicObject selfObj = (RubyBasicObject) self;
int h = selfObj.getType().hashCode();
VariableAccessor[] accessors = getStructAccessors(selfObj);
for (int i = 0; i < accessors.length; i++) {
h = (h 0) {
str.catString(", ");
} else if (first != '#') {
str.catString(" ");
}
RubySymbol id = members.eltOk(i);
if (id.validLocalVariableName() || id.validConstantName()) {
str.append(id.fstring());
} else {
str.append(id.inspect(context));
}
str.catString("=");
str.append(rbInspect(context, (IRubyObject) accessors[i].get(s)));
}
str.catString(">");
return str;
}
private static RubyArray getStructMembers(IRubyObject s) {
RubyClass metaClass = s.getMetaClass();
return getMembersFromClass(metaClass);
}
private static RubyArray getMembersFromClass(RubyClass metaClass) {
while (metaClass != null) {
RubyArray members = (RubyArray) metaClass.getInternalVariable(MEMBERS_KEY);
if (members != null) return members;
metaClass = metaClass.getSuperClass();
}
throw new RuntimeException("non-Data attempted to access Data members");
}
private static VariableAccessor[] getStructAccessors(IRubyObject s) {
RubyClass metaClass = s.getMetaClass();
return getAccessorsFromClass(metaClass);
}
private static VariableAccessor[] getAccessorsFromClass(RubyClass metaClass) {
while (metaClass != null) {
VariableAccessor[] accessors = (VariableAccessor[]) metaClass.getInternalVariable(ACCESSORS_KEY);
if (accessors != null) return accessors;
metaClass = metaClass.getSuperClass();
}
throw new RuntimeException("non-Data attempted to access Data accessors");
}
private static VariableTableManager getVariableManagerFromClass(RubyClass metaClass) {
while (metaClass != null) {
VariableAccessor[] accessors = (VariableAccessor[]) metaClass.getInternalVariable(ACCESSORS_KEY);
if (accessors != null) return metaClass.getVariableTableManager();
metaClass = metaClass.getSuperClass();
}
throw new RuntimeException("non-Data attempted to access Data accessors");
}
private static IRubyObject hashData(ThreadContext ctx, RubyBasicObject state, IRubyObject obj, boolean recur) {
return recur ? asFixnum(ctx, 0) : invokedynamic(ctx, obj, HASH);
}
private static IRubyObject eqlData(ThreadContext c, RubyBasicObject s, IRubyObject o, boolean recur) {
if (recur) return c.tru;
VariableAccessor[] accessors = getStructAccessors(s);
for (int i = 0; i < accessors.length; i++) {
VariableAccessor accessor = accessors[i];
if (!RubyBasicObject.eqlInternal(c, ((RubyBasicObject) accessor.get(s)), (IRubyObject) accessor.get(o))) {
return c.fals;
}
}
return c.tru;
}
private static IRubyObject equalData(ThreadContext c, RubyBasicObject s, IRubyObject o, boolean recur) {
if (recur) return c.tru;
VariableAccessor[] accessors = getStructAccessors(s);
for (int i = 0; i < accessors.length; i++) {
VariableAccessor accessor = accessors[i];
if (!RubyBasicObject.equalInternal(c, ((RubyBasicObject) accessor.get(s)), (IRubyObject) accessor.get(o))) {
return c.fals;
}
}
return c.tru;
}
}